diff --git a/404.html b/404.html index 0b2c4ddcbb..7f68171fa7 100644 --- a/404.html +++ b/404.html @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -4675,6 +4675,8 @@ + + @@ -4811,6 +4813,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5584,6 +5607,8 @@ + + @@ -5678,6 +5703,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6123,10 +6169,10 @@

    404 - Not found

    - + - + diff --git a/FAQ/index.html b/FAQ/index.html index 22653a735d..c840e32195 100644 --- a/FAQ/index.html +++ b/FAQ/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4826,6 +4828,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5693,6 +5718,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6758,10 +6804,10 @@

    Why doesn't the - + - + diff --git a/api/app/index.html b/api/app/index.html index b49db90aaf..c81e4e93f3 100644 --- a/api/app/index.html +++ b/api/app/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4927,6 +4929,24 @@ +

  • + +
  • + + + app_resume_signal + + + +
  • + +
  • + + + app_suspend_signal + + +
  • @@ -5224,6 +5244,15 @@ +
  • + +
  • + + + action_suspend_process() + + +
  • @@ -5647,6 +5676,15 @@ +
  • + +
  • + + + suspend() + + +
  • @@ -5760,6 +5798,15 @@ +
  • + +
  • + + + SuspendNotSupported + + +
  • @@ -5858,6 +5905,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6631,6 +6699,8 @@ + + @@ -6725,6 +6795,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7240,6 +7331,24 @@ +
  • + +
  • + + + app_resume_signal + + + +
  • + +
  • + + + app_suspend_signal + + +
  • @@ -7537,6 +7646,15 @@ +
  • + +
  • + + + action_suspend_process() + + +
  • @@ -7960,6 +8078,15 @@ +
  • + +
  • + + + suspend() + + +
  • @@ -8073,6 +8200,15 @@ +
  • + +
  • + + + SuspendNotSupported + + +
  • @@ -8554,8 +8690,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8580,8 +8716,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8606,8 +8742,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8627,6 +8763,61 @@

    +

    + app_resume_signal + + + + instance-attribute + + +

    +
    app_resume_signal = Signal(self, 'app-resume')
    +
    + +
    + +

    The signal that is published when the app is resumed after a suspend.

    +

    When the app is resumed after a +App.suspend call this signal will be +published; +subscribe to this signal to +perform work after the app has resumed.

    +
    + + + +
    + + + +

    + app_suspend_signal + + + + instance-attribute + + +

    +
    app_suspend_signal = Signal(self, 'app-suspend')
    +
    + +
    + +

    The signal that is published when the app is suspended.

    +

    When App.suspend is called this signal +will be published; +subscribe to this signal to +perform work before the suspension takes place.

    +
    + +
    + +
    + + +

    children @@ -8724,8 +8915,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9054,7 +9245,7 @@

    -
    screen_stack: Sequence[Screen]
    +
    screen_stack: Sequence[Screen[Any]]
     
    @@ -9072,7 +9263,7 @@
    Returns
    - Sequence[Screen] + Sequence[Screen[Any]]

    A snapshot of the current state of the screen stack.

    @@ -9178,8 +9369,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9210,8 +9401,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9788,6 +9979,35 @@
    Parameters
    +

    + action_suspend_process + + + method + + +

    +
    def action_suspend_process(self):
    +
    + +
    + +

    Suspend the process into the background.

    + +
    + Note +

    On Unix and Unix-like systems a SIGTSTP is sent to the +application's process. Currently on Windows and when running +under Textual Web this is a non-operation.

    +
    +
    + +
    + +
    + + +

    action_switch_mode @@ -13026,6 +13246,64 @@

    Parameters
    +

    + suspend + + + method + + +

    +
    def suspend(self):
    +
    + +
    + +

    A context manager that temporarily suspends the app.

    +

    While inside the with block, the app will stop reading input and +emitting output. Other applications will have full control of the +terminal, configured as it was before the app started running. When +the with block ends, the application will start reading input and +emitting output again.

    + +
    + Example +
    with self.suspend():
    +    os.system("emacs -nw")
    +
    +
    +
    Raises
    + + + + + + + + + + + + + +
    TypeDescription
    + SuspendNotSupported +

    If the environment doesn't support suspending.

    +
    +

    Note

    +

    Suspending the application is currently only supported on +Unix-like operating systems and Microsoft Windows. Suspending is +not supported in Textual Web.

    +
    + +
    + +
    + +
    + + +

    switch_mode @@ -13485,6 +13763,38 @@

    +

    + SuspendNotSupported + + + + class + + +

    + + +
    +

    + Bases: Exception

    + + +

    Raised if suspending the application is not supported.

    +

    This exception is raised if App.suspend is called while +the application is running in an environment where this isn't supported.

    + + + +
    + +
    + +
    + + + + +

    UnknownModeError @@ -13676,10 +13986,10 @@

    Returns
    - + - + diff --git a/api/await_complete/index.html b/api/await_complete/index.html index 2b12e3b1a4..9e168b45b2 100644 --- a/api/await_complete/index.html +++ b/api/await_complete/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6505,10 +6551,10 @@

  • - + - + diff --git a/api/await_remove/index.html b/api/await_remove/index.html index ab28eeb7cf..ff2631abde 100644 --- a/api/await_remove/index.html +++ b/api/await_remove/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4880,6 +4882,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5653,6 +5676,8 @@ + + @@ -5747,6 +5772,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6385,10 +6431,10 @@
    Parameters
    - + - + diff --git a/api/binding/index.html b/api/binding/index.html index d4ec6c7a66..53f7d9bed3 100644 --- a/api/binding/index.html +++ b/api/binding/index.html @@ -14,14 +14,14 @@ - + - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4967,6 +4969,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5740,6 +5763,8 @@ + + @@ -5834,6 +5859,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6417,8 +6463,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6464,8 +6510,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6488,8 +6534,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6512,8 +6558,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6681,13 +6727,13 @@

    - + - + - + diff --git a/api/cache/index.html b/api/cache/index.html new file mode 100644 index 0000000000..ed0dd9c6a7 --- /dev/null +++ b/api/cache/index.html @@ -0,0 +1,7234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cache - Textual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + Skip to content + + +
    +
    + +
    + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    + + + + + + + +

    Cache

    + +
    + + + +
    + +

    Containers to implement caching.

    +

    These are used in Textual to avoid recalculating expensive operations, such as rendering.

    + + + +
    + + + + + + + + +
    + + + + + +

    + FIFOCache + + + + class + + +

    +
    def __init__(self, maxsize):
    +
    + +
    +

    + Bases: Generic[CacheKey, CacheValue]

    + + +

    A simple cache that discards the first added key when full (First In First Out).

    +

    This has a lower overhead than LRUCache, but won't manage a working set as efficiently. +It is most suitable for a cache with a relatively low maximum size that is not expected to +do many lookups.

    + + + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + maxsize +
    + int +
    + required +

    Maximum size of cache before discarding items.

    + + + + +
    + + + + + + + + + +
    + + + +

    + clear + + + method + + +

    +
    def clear(self):
    +
    + +
    + +

    Clear the cache.

    + +
    + +
    + +
    + + + +

    + get + + + method + + +

    +
    def get(self, key, default=None):
    +
    + +
    + +

    Get a value from the cache, or return a default if the key is not present.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + key +
    + CacheKey +
    + required +

    Key

    + default +
    + DefaultValue | None +
    + None +

    Default to return if key is not present.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + CacheValue | DefaultValue | None +

    Either the value or a default.

    + +
    + +
    + +
    + + + +

    + keys + + + method + + +

    +
    def keys(self):
    +
    + +
    + +

    Get cache keys.

    + +
    + +
    + +
    + + + +

    + set + + + method + + +

    +
    def set(self, key, value):
    +
    + +
    + +

    Set a value.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + key +
    + CacheKey +
    + required +

    Key.

    + value +
    + CacheValue +
    + required +

    Value.

    + +
    + +
    + + + +
    + +
    + +
    + +
    + + + + + +

    + LRUCache + + + + class + + +

    +
    def __init__(self, maxsize):
    +
    + +
    +

    + Bases: Generic[CacheKey, CacheValue]

    + + +

    A dictionary-like container with a maximum size.

    +

    If an additional item is added when the LRUCache is full, the least +recently used key is discarded to make room for the new item.

    +

    The implementation is similar to functools.lru_cache, which uses a (doubly) +linked list to keep track of the most recently used items.

    +

    Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference +to the previous entry, and NEXT is a reference to the next value.

    +

    Note that stdlib's @lru_cache is implemented in C and faster! It's best to use +@lru_cache where you are caching things that are fairly quick and called many times. +Use LRUCache where you want increased flexibility and you are caching slow operations +where the overhead of the cache is a small fraction of the total processing time.

    + + + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + maxsize +
    + int +
    + required +

    Maximum size of the cache, before old items are discarded.

    + + + + +
    + + + + + + + +
    + + + +

    + maxsize + + + + writable + property + + +

    +
    maxsize: int
    +
    + +
    + + +
    + +
    + + + +
    + + + +

    + clear + + + method + + +

    +
    def clear(self):
    +
    + +
    + +

    Clear the cache.

    + +
    + +
    + +
    + + + +

    + discard + + + method + + +

    +
    def discard(self, key):
    +
    + +
    + +

    Discard item in cache from key.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + key +
    + CacheKey +
    + required +

    Cache key.

    + +
    + +
    + +
    + + + +

    + get + + + method + + +

    +
    def get(self, key, default=None):
    +
    + +
    + +

    Get a value from the cache, or return a default if the key is not present.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + key +
    + CacheKey +
    + required +

    Key

    + default +
    + DefaultValue | None +
    + None +

    Default to return if key is not present.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + CacheValue | DefaultValue | None +

    Either the value or a default.

    + +
    + +
    + +
    + + + +

    + grow + + + method + + +

    +
    def grow(self, maxsize):
    +
    + +
    + +

    Grow the maximum size to at least maxsize elements.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + maxsize +
    + int +
    + required +

    New maximum size.

    + +
    + +
    + +
    + + + +

    + keys + + + method + + +

    +
    def keys(self):
    +
    + +
    + +

    Get cache keys.

    + +
    + +
    + +
    + + + +

    + set + + + method + + +

    +
    def set(self, key, value):
    +
    + +
    + +

    Set a value.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + key +
    + CacheKey +
    + required +

    Key.

    + value +
    + CacheValue +
    + required +

    Value.

    + +
    + +
    + + + +
    + +
    + +
    + + + + +
    + +
    + +
    + + + + + + + + + + + + + +
    +
    + + + +
    + +
    + + + +
    +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/api/color/index.html b/api/color/index.html index 6c4e941edc..f746f4deb9 100644 --- a/api/color/index.html +++ b/api/color/index.html @@ -11,7 +11,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4824,6 +4826,27 @@ + + +
  • + + + + + Cache + + + + +
  • + + + + + + + + @@ -6103,6 +6126,8 @@ + + @@ -6197,6 +6222,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7920,8 +7966,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8910,8 +8956,8 @@

    parse - classmethod cached + classmethod

    @@ -9787,7 +9833,7 @@

    +

  • + +
  • + + + Suspending + + + + +
  • @@ -4901,6 +4934,8 @@ + + @@ -5037,6 +5072,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5810,6 +5866,8 @@ + + @@ -5904,6 +5962,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6405,6 +6484,39 @@ +
  • + +
  • + + + Suspending + + + + +
  • @@ -7652,6 +7764,387 @@

    Return code import sys sys.exit(app.return_code or 0) +

    Suspending

    +

    A Textual app may be suspended so you can leave application mode for a period of time. +This is often used to temporarily replace your app with another terminal application.

    +

    You could use this to allow the user to edit content with their preferred text editor, for example.

    +
    +

    Info

    +

    App suspension is unavailable with textual-web.

    +
    +

    Suspend context manager

    +

    You can use the App.suspend context manager to suspend your app. +The following Textual app will launch vim (a text editor) when the user clicks a button:

    +
    +
    +
    +
    from os import system
    +
    +from textual import on
    +from textual.app import App, ComposeResult
    +from textual.widgets import Button
    +
    +
    +class SuspendingApp(App[None]):
    +
    +    def compose(self) -> ComposeResult:
    +        yield Button("Open the editor", id="edit")
    +
    +    @on(Button.Pressed, "#edit")
    +    def run_external_editor(self) -> None:
    +        with self.suspend():  # (1)!
    +            system("vim")
    +
    +
    +if __name__ == "__main__":
    +    SuspendingApp().run()
    +
    +
      +
    1. All code in the body of the with statement will be run while the app is suspended.
    2. +
    +
    +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SuspendingApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Open the editor +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + + +

    +
    +
    +
    +

    Suspending from foreground

    +

    On Unix and Unix-like systems (GNU/Linux, macOS, etc) Textual has support for the user pressing a key combination to suspend the application as the foreground process. +Ordinarily this key combination is Ctrl+Z; +in a Textual application this is disabled by default, but an action is provided (action_suspend_process) that you can bind in the usual way. +For example:

    +
    +
    +
    +
    from textual.app import App, ComposeResult
    +from textual.binding import Binding
    +from textual.widgets import Label
    +
    +
    +class SuspendKeysApp(App[None]):
    +
    +    BINDINGS = [Binding("ctrl+z", "suspend_process")]
    +
    +    def compose(self) -> ComposeResult:
    +        yield Label("Press Ctrl+Z to suspend!")
    +
    +
    +if __name__ == "__main__":
    +    SuspendKeysApp().run()
    +
    +
    +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SuspendKeysApp + + + + + + + + + + Press Ctrl+Z to suspend! + + + + + + + + + + + + + + + + + + + + + + + + + + +

    +
    +
    +
    +
    +

    Note

    +

    If suspend_process is called on Windows, or when your application is being hosted under Textual Web, the call will be ignored.

    +

    CSS

    Textual apps can reference CSS files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).

    @@ -7660,49 +8153,49 @@

    CSS¶<

    The chapter on Textual CSS describes how to use CSS in detail. For now let's look at how your app references external CSS files.

    The following example enables loading of CSS by adding a CSS_PATH class variable:

    -
    question02.py
    from textual.app import App, ComposeResult
    -from textual.widgets import Button, Label
    -
    -
    -class QuestionApp(App[str]):
    -    CSS_PATH = "question02.tcss"
    -
    -    def compose(self) -> ComposeResult:
    -        yield Label("Do you love Textual?", id="question")
    -        yield Button("Yes", id="yes", variant="primary")
    -        yield Button("No", id="no", variant="error")
    -
    -    def on_button_pressed(self, event: Button.Pressed) -> None:
    -        self.exit(event.button.id)
    -
    -
    -if __name__ == "__main__":
    -    app = QuestionApp()
    -    reply = app.run()
    -    print(reply)
    +
    question02.py
    from textual.app import App, ComposeResult
    +from textual.widgets import Button, Label
    +
    +
    +class QuestionApp(App[str]):
    +    CSS_PATH = "question02.tcss"
    +
    +    def compose(self) -> ComposeResult:
    +        yield Label("Do you love Textual?", id="question")
    +        yield Button("Yes", id="yes", variant="primary")
    +        yield Button("No", id="no", variant="error")
    +
    +    def on_button_pressed(self, event: Button.Pressed) -> None:
    +        self.exit(event.button.id)
    +
    +
    +if __name__ == "__main__":
    +    app = QuestionApp()
    +    reply = app.run()
    +    print(reply)
     

    Note

    We also added an id to the Label, because we want to style it in the CSS.

    If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references "question01.tcss" in the same directory as the Python code. Here is that CSS file:

    -
    question02.tcss
    Screen {
    -    layout: grid;
    -    grid-size: 2;
    -    grid-gutter: 2;
    -    padding: 2;
    -}
    -#question {
    -    width: 100%;
    -    height: 100%;
    -    column-span: 2;
    -    content-align: center bottom;
    -    text-style: bold;
    -}
    -
    -Button {
    -    width: 100%;
    -}
    +
    question02.tcss
    Screen {
    +    layout: grid;
    +    grid-size: 2;
    +    grid-gutter: 2;
    +    padding: 2;
    +}
    +#question {
    +    width: 100%;
    +    height: 100%;
    +    column-span: 2;
    +    content-align: center bottom;
    +    text-style: bold;
    +}
    +
    +Button {
    +    width: 100%;
    +}
     

    When "question02.py" runs it will load "question02.tcss" and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:

    @@ -7867,72 +8360,72 @@

    Classvar CSSWhile external CSS files are recommended for most applications, and enable some cool features like live editing, you can also specify the CSS directly within the Python code.

    To do this set a CSS class variable on the app to a string containing your CSS.

    Here's the question app with classvar CSS:

    -
    question03.py
    from textual.app import App, ComposeResult
    -from textual.widgets import Label, Button
    -
    -
    -class QuestionApp(App[str]):
    -    CSS = """
    -    Screen {
    -        layout: grid;
    -        grid-size: 2;
    -        grid-gutter: 2;
    -        padding: 2;
    -    }
    -    #question {
    -        width: 100%;
    -        height: 100%;
    -        column-span: 2;
    -        content-align: center bottom;
    -        text-style: bold;
    -    }
    -
    -    Button {
    -        width: 100%;
    -    }
    -    """
    -
    -    def compose(self) -> ComposeResult:
    -        yield Label("Do you love Textual?", id="question")
    -        yield Button("Yes", id="yes", variant="primary")
    -        yield Button("No", id="no", variant="error")
    -
    -    def on_button_pressed(self, event: Button.Pressed) -> None:
    -        self.exit(event.button.id)
    -
    -
    -if __name__ == "__main__":
    -    app = QuestionApp()
    -    reply = app.run()
    -    print(reply)
    +
    question03.py
    from textual.app import App, ComposeResult
    +from textual.widgets import Label, Button
    +
    +
    +class QuestionApp(App[str]):
    +    CSS = """
    +    Screen {
    +        layout: grid;
    +        grid-size: 2;
    +        grid-gutter: 2;
    +        padding: 2;
    +    }
    +    #question {
    +        width: 100%;
    +        height: 100%;
    +        column-span: 2;
    +        content-align: center bottom;
    +        text-style: bold;
    +    }
    +
    +    Button {
    +        width: 100%;
    +    }
    +    """
    +
    +    def compose(self) -> ComposeResult:
    +        yield Label("Do you love Textual?", id="question")
    +        yield Button("Yes", id="yes", variant="primary")
    +        yield Button("No", id="no", variant="error")
    +
    +    def on_button_pressed(self, event: Button.Pressed) -> None:
    +        self.exit(event.button.id)
    +
    +
    +if __name__ == "__main__":
    +    app = QuestionApp()
    +    reply = app.run()
    +    print(reply)
     

    Title and subtitle

    Textual apps have a title attribute which is typically the name of your application, and an optional sub_title attribute which adds additional context (such as the file your are working on). By default, title will be set to the name of your App class, and sub_title is empty. You can change these defaults by defining TITLE and SUB_TITLE class variables. Here's an example of that:

    -
    question_title01.py
    from textual.app import App, ComposeResult
    -from textual.widgets import Button, Header, Label
    -
    -
    -class MyApp(App[str]):
    -    CSS_PATH = "question02.tcss"
    -    TITLE = "A Question App"
    -    SUB_TITLE = "The most important question"
    -
    -    def compose(self) -> ComposeResult:
    -        yield Header()
    -        yield Label("Do you love Textual?", id="question")
    -        yield Button("Yes", id="yes", variant="primary")
    -        yield Button("No", id="no", variant="error")
    -
    -    def on_button_pressed(self, event: Button.Pressed) -> None:
    -        self.exit(event.button.id)
    -
    -
    -if __name__ == "__main__":
    -    app = MyApp()
    -    reply = app.run()
    -    print(reply)
    +
    question_title01.py
    from textual.app import App, ComposeResult
    +from textual.widgets import Button, Header, Label
    +
    +
    +class MyApp(App[str]):
    +    CSS_PATH = "question02.tcss"
    +    TITLE = "A Question App"
    +    SUB_TITLE = "The most important question"
    +
    +    def compose(self) -> ComposeResult:
    +        yield Header()
    +        yield Label("Do you love Textual?", id="question")
    +        yield Button("Yes", id="yes", variant="primary")
    +        yield Button("No", id="no", variant="error")
    +
    +    def on_button_pressed(self, event: Button.Pressed) -> None:
    +        self.exit(event.button.id)
    +
    +
    +if __name__ == "__main__":
    +    app = MyApp()
    +    reply = app.run()
    +    print(reply)
     

    Note that the title and subtitle are displayed by the builtin Header widget at the top of the screen:

    @@ -8096,34 +8589,34 @@

    Title and subtitlequestion_title02.py
    from textual.app import App, ComposeResult
    -from textual.events import Key
    -from textual.widgets import Button, Header, Label
    -
    -
    -class MyApp(App[str]):
    -    CSS_PATH = "question02.tcss"
    -    TITLE = "A Question App"
    -    SUB_TITLE = "The most important question"
    -
    -    def compose(self) -> ComposeResult:
    -        yield Header()
    -        yield Label("Do you love Textual?", id="question")
    -        yield Button("Yes", id="yes", variant="primary")
    -        yield Button("No", id="no", variant="error")
    -
    -    def on_button_pressed(self, event: Button.Pressed) -> None:
    -        self.exit(event.button.id)
    -
    -    def on_key(self, event: Key):
    -        self.title = event.key
    -        self.sub_title = f"You just pressed {event.key}!"
    -
    -
    -if __name__ == "__main__":
    -    app = MyApp()
    -    reply = app.run()
    -    print(reply)
    +
    question_title02.py
    from textual.app import App, ComposeResult
    +from textual.events import Key
    +from textual.widgets import Button, Header, Label
    +
    +
    +class MyApp(App[str]):
    +    CSS_PATH = "question02.tcss"
    +    TITLE = "A Question App"
    +    SUB_TITLE = "The most important question"
    +
    +    def compose(self) -> ComposeResult:
    +        yield Header()
    +        yield Label("Do you love Textual?", id="question")
    +        yield Button("Yes", id="yes", variant="primary")
    +        yield Button("No", id="no", variant="error")
    +
    +    def on_button_pressed(self, event: Button.Pressed) -> None:
    +        self.exit(event.button.id)
    +
    +    def on_key(self, event: Key):
    +        self.title = event.key
    +        self.sub_title = f"You just pressed {event.key}!"
    +
    +
    +if __name__ == "__main__":
    +    app = MyApp()
    +    reply = app.run()
    +    print(reply)
     

    If you run this app and press the T key, you should see the header update accordingly:

    @@ -8410,10 +8903,10 @@

    What's next{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/command_palette/index.html b/guide/command_palette/index.html index 6c005a1cce..096e3a2f5f 100644 --- a/guide/command_palette/index.html +++ b/guide/command_palette/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4808,6 +4808,8 @@ + + @@ -4944,6 +4946,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5717,6 +5740,8 @@ + + @@ -5811,6 +5836,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6803,9 +6849,9 @@

    Command providersfrom textual.widgets import Static -class PythonFileCommands(Provider): - """A command provider to open a Python file in the current working directory.""" - +class PythonFileCommands(Provider): + """A command provider to open a Python file in the current working directory.""" + def read_files(self) -> list[Path]: """Get a list of Python files in the current working directory.""" return list(Path("./").glob("*.py")) @@ -6832,13 +6878,13 @@

    Command providers partial(app.open_file, path), help="Open this file in the viewer", ) - - -class ViewerApp(App): + + +class ViewerApp(App): """Demonstrate a command source.""" - - COMMANDS = App.COMMANDS | {PythonFileCommands} # (6)! - + + COMMANDS = App.COMMANDS | {PythonFileCommands} # (6)! + def compose(self) -> ComposeResult: with VerticalScroll(): yield Static(id="code", expand=True) @@ -7024,10 +7070,10 @@

    Disabling the command palette{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/design/index.html b/guide/design/index.html index 6a54948b67..f94d35fd72 100644 --- a/guide/design/index.html +++ b/guide/design/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4808,6 +4808,8 @@ + + @@ -4944,6 +4946,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5717,6 +5740,8 @@ + + @@ -5811,6 +5836,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7651,10 +7697,10 @@

    Theme Reference{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/devtools/index.html b/guide/devtools/index.html index 02601e395b..85e6b918bc 100644 --- a/guide/devtools/index.html +++ b/guide/devtools/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4838,6 +4838,8 @@ + + @@ -4974,6 +4976,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5747,6 +5770,8 @@ + + @@ -5841,6 +5866,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6736,10 +6782,10 @@

    Logging handler{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/events/index.html b/guide/events/index.html index f121a53ad1..0d41121b12 100644 --- a/guide/events/index.html +++ b/guide/events/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4895,6 +4895,8 @@ + + @@ -5031,6 +5033,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5804,6 +5827,8 @@ + + @@ -5898,6 +5923,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6515,7 +6561,7 @@

    Message QueueDefault behaviors

    You may be familiar with Python's super function to call a function defined in a base class. You will not have to use this in event handlers as Textual will automatically call handler methods defined in a widget's base class(es).

    -

    For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Left and Right keys), then Static.on_key, and finally Widget.on_key.

    +

    For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Up and Down keys), then Static.on_key, and finally Widget.on_key.

    Preventing default behaviors

    If you don't want this behavior you can call prevent_default() on the event object. This tells Textual not to call any more handlers on base classes.

    @@ -7873,10 +7919,10 @@

    Async handlers{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/index.html b/guide/index.html index f344b4b4a4..617a51e2db 100644 --- a/guide/index.html +++ b/guide/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4826,6 +4828,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5693,6 +5718,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6217,10 +6263,10 @@

    Example code - + - + diff --git a/guide/input/index.html b/guide/input/index.html index bc51e24650..cc0eda5163 100644 --- a/guide/input/index.html +++ b/guide/input/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4949,6 +4949,8 @@ + + @@ -5085,6 +5087,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5858,6 +5881,8 @@ + + @@ -5952,6 +5977,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7311,16 +7357,16 @@

    Mouse movements class MouseApp(App): - CSS_PATH = "mouse01.tcss" - - def compose(self) -> ComposeResult: - yield RichLog() + CSS_PATH = "mouse01.tcss" + + def compose(self) -> ComposeResult: + yield RichLog() yield Ball("Textual") - def on_mouse_move(self, event: events.MouseMove) -> None: - self.screen.query_one(RichLog).write(event) - self.query_one(Ball).offset = event.screen_offset - (8, 2) - + def on_mouse_move(self, event: events.MouseMove) -> None: + self.screen.query_one(RichLog).write(event) + self.query_one(Ball).offset = event.screen_offset - (8, 2) + if __name__ == "__main__": app = MouseApp() @@ -7490,10 +7536,10 @@

    Scroll events - + - + diff --git a/guide/layout/index.html b/guide/layout/index.html index a92fe51c03..ce5f11076d 100644 --- a/guide/layout/index.html +++ b/guide/layout/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4859,6 +4859,8 @@ + + @@ -4995,6 +4997,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5768,6 +5791,8 @@ + + @@ -5862,6 +5887,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -10532,10 +10578,10 @@

    Putting it all together - + - + diff --git a/guide/queries/index.html b/guide/queries/index.html index 1c9c98f99a..3b7bce704f 100644 --- a/guide/queries/index.html +++ b/guide/queries/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4817,6 +4817,8 @@ + + @@ -4953,6 +4955,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5726,6 +5749,8 @@ + + @@ -5820,6 +5845,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6522,10 +6568,10 @@

    Loop-free operations{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/reactivity/index.html b/guide/reactivity/index.html index 336f2eeb43..713e58b8bc 100644 --- a/guide/reactivity/index.html +++ b/guide/reactivity/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4847,6 +4847,8 @@ + + @@ -4983,6 +4985,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5756,6 +5779,8 @@ + + @@ -5850,6 +5875,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7873,10 +7919,10 @@

    Compute methods{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/screens/index.html b/guide/screens/index.html index c26641f1d0..23d96bb41a 100644 --- a/guide/screens/index.html +++ b/guide/screens/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4919,6 +4919,8 @@ + + @@ -5055,6 +5057,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5828,6 +5851,8 @@ + + @@ -5922,6 +5947,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -8808,10 +8854,10 @@

    Modes&

  • - + - + diff --git a/guide/styles/index.html b/guide/styles/index.html index e92a3298c1..2de8c77131 100644 --- a/guide/styles/index.html +++ b/guide/styles/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4922,6 +4922,8 @@ + + @@ -5058,6 +5060,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5831,6 +5854,8 @@ + + @@ -5925,6 +5950,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -8841,133 +8887,133 @@

    Border + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderApp + BorderApp - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings  -total obliteration. -I will face my fear. -I will permit it to pass over me and  -through me. -And when it has gone past, I will turn  -the inner eye to see its path. -Where the fear has gone there will be  -nothing. Only I will remain. -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - - - - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings  +total obliteration. +I will face my fear. +I will permit it to pass over me and  +through me. +And when it has gone past, I will turn +the inner eye to see its path. +Where the fear has gone there will be  +nothing. Only I will remain. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + + + + + @@ -9037,133 +9083,133 @@

    Title alignment + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderTitleApp + BorderTitleApp - - - - ━━━━━━━━━ Litany Against Fear ━━━━━━━━━ -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings  -total obliteration. -I will face my fear. -I will permit it to pass over me and  -through me. -And when it has gone past, I will turn  -the inner eye to see its path. -Where the fear has gone there will be  -nothing. Only I will remain. -━━━━━━━━━ by Frank Herbert, in “Dune”  - - - - - - - - - - + + + + ━━━━━━━━ Litany Against Fear ━━━━━━━━━ +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings  +total obliteration. +I will face my fear. +I will permit it to pass over me and  +through me. +And when it has gone past, I will turn +the inner eye to see its path. +Where the fear has gone there will be  +nothing. Only I will remain. +━━━━━━━━ by Frank Herbert, in “Dune”  + + + + + + + + + + @@ -9914,10 +9960,10 @@

    More styles - + - + diff --git a/guide/testing/index.html b/guide/testing/index.html index a30ca47036..c55766820d 100644 --- a/guide/testing/index.html +++ b/guide/testing/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4913,6 +4913,8 @@ + + @@ -5049,6 +5051,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5822,6 +5845,8 @@ + + @@ -5916,6 +5941,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7275,10 +7321,10 @@

    Running setup code{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/widgets/index.html b/guide/widgets/index.html index 09f5f9a3d0..7b7449bbcc 100644 --- a/guide/widgets/index.html +++ b/guide/widgets/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5018,6 +5018,8 @@ + + @@ -5154,6 +5156,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5927,6 +5950,8 @@ + + @@ -6021,6 +6046,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7555,7 +7601,7 @@

    Scoped CSSDefault specificity

    CSS defined within DEFAULT_CSS has an automatically lower specificity than CSS read from either the App's CSS class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.

    @@ -9258,138 +9304,138 @@

    Loading indicator + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DataApp + DataApp - + - - - - - - -● ● ● ● ● ● ● ●  - - - - - - - - - - - -● ● ● ● ● ● ● ●  - - - - - + + + + + + +● ● ● ● ● ● ● ●  + + + + + + + + + + + +● ● ● ● ● ● ● ●  + + + + + @@ -11813,10 +11859,10 @@

    Attributes down{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/guide/workers/index.html b/guide/workers/index.html index 2473265d5a..b0dc60b209 100644 --- a/guide/workers/index.html +++ b/guide/workers/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4832,6 +4832,8 @@ + + @@ -4968,6 +4970,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5741,6 +5764,8 @@ + + @@ -5835,6 +5860,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6995,10 +7041,10 @@

    Posting messages{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/help/index.html b/help/index.html index 0f10e8fcdd..7ded137e3c 100644 --- a/help/index.html +++ b/help/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4757,6 +4757,8 @@ + + @@ -4893,6 +4895,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5666,6 +5689,8 @@ + + @@ -5760,6 +5785,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6305,10 +6351,10 @@

    Discord Server{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/how-to/center-things/index.html b/how-to/center-things/index.html index 9f176a2819..f877a2eb3d 100644 --- a/how-to/center-things/index.html +++ b/how-to/center-things/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4688,6 +4688,8 @@ + + @@ -4824,6 +4826,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5778,6 +5803,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -8229,10 +8275,10 @@

    Summary{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/how-to/design-a-layout/index.html b/how-to/design-a-layout/index.html index b0caa2408e..97c7c2443e 100644 --- a/how-to/design-a-layout/index.html +++ b/how-to/design-a-layout/index.html @@ -14,14 +14,14 @@ - + - + @@ -29,7 +29,7 @@ - + @@ -4688,6 +4688,8 @@ + + @@ -4824,6 +4826,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5787,6 +5812,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7702,13 +7748,13 @@

    Summary + @@ -6874,10 +6920,10 @@

    Summary{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/index.html b/index.html index 4c35c6005c..bf3be9ed9d 100644 --- a/index.html +++ b/index.html @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -4737,6 +4737,8 @@ + + @@ -4873,6 +4875,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5646,6 +5669,8 @@ + + @@ -5740,6 +5765,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6595,169 +6641,169 @@

    What is Textual? + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StopwatchApp + StopwatchApp - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:04.07 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - D  Toggle dark mode  A  Add  R  Remove  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:04.04 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + D  Toggle dark mode  A  Add  R  Remove  @@ -7210,10 +7256,10 @@

    What is Textual?{"base": ".", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/objects.inv b/objects.inv index 3e079befe6..3196fdb2a2 100644 Binary files a/objects.inv and b/objects.inv differ diff --git a/reference/index.html b/reference/index.html index 926b9c9377..4a66fbb6f6 100644 --- a/reference/index.html +++ b/reference/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4826,6 +4828,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5693,6 +5718,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6225,10 +6271,10 @@

    Reference{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/roadmap/index.html b/roadmap/index.html index 676a44dd59..09914a63a6 100644 --- a/roadmap/index.html +++ b/roadmap/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4690,6 +4690,8 @@ + + @@ -4826,6 +4828,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5599,6 +5622,8 @@ + + @@ -5693,6 +5718,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6385,10 +6431,10 @@

    Widgets{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/search/search_index.json b/search/search_index.json index ef47984e72..7f24c41444 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

    Tip

    See the navigation links in the header or side-bar.

    Click (top left) on mobile.

    "},{"location":"#welcome","title":"Welcome","text":"

    Welcome to the Textual framework documentation.

    Get started or go straight to the Tutorial

    "},{"location":"#what-is-textual","title":"What is Textual?","text":"

    Textual is a Rapid Application Development framework for Python, built by Textualize.io.

    Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal or a web browser!

    • Rapid development

      Uses your existing Python skills to build beautiful user interfaces.

    • Low requirements

      Run Textual on a single board computer if you want to.

    • Cross platform

      Textual runs just about everywhere.

    • Remote

      Textual apps can run over SSH.

    • CLI Integration

      Textual apps can be launched and run from the command prompt.

    • Open Source

      Textual is licensed under MIT.

    CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 C+/-%\u00f7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 789\u00d7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 456- \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 123+ \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 0.= \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    PrideApp

    StopwatchApp \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:04.07 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2586\u2586\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a05\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a06\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"FAQ/","title":"FAQ","text":""},{"location":"FAQ/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

    Welcome to the Textual FAQ. Here we try and answer any question that comes up frequently. If you can't find what you are looking for here, see our other help channels.

    "},{"location":"FAQ/#does-textual-support-images","title":"Does Textual support images?","text":"

    Textual doesn't have built-in support for images yet, but it is on the Roadmap.

    See also the rich-pixels project for a Rich renderable for images that works with Textual.

    "},{"location":"FAQ/#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp","title":"How can I fix ImportError cannot import name ComposeResult from textual.app ?","text":"

    You likely have an older version of Textual. You can install the latest version by adding the -U switch which will force pip to upgrade.

    The following should do it:

    pip install textual-dev -U\n

    "},{"location":"FAQ/#how-can-i-select-and-copy-text-in-a-textual-app","title":"How can I select and copy text in a Textual app?","text":"

    Running a Textual app puts your terminal in to application mode which disables clicking and dragging to select text. Most terminal emulators offer a modifier key which you can hold while you click and drag to restore the behavior you may expect from the command line. The exact modifier key depends on the terminal and platform you are running on.

    • iTerm Hold the OPTION key.
    • Gnome Terminal Hold the SHIFT key.
    • Windows Terminal Hold the SHIFT key.

    Refer to the documentation for your terminal emulator, if it is not listed above.

    "},{"location":"FAQ/#how-can-i-set-a-translucent-app-background","title":"How can I set a translucent app background?","text":"

    Some terminal emulators have a translucent background feature which allows the desktop underneath to be partially visible.

    This feature is unlikely to work with Textual, as the translucency effect requires the use of ANSI background colors, which Textual doesn't use. Textual uses 16.7 million colors where available which enables consistent colors across all platforms and additional effects which aren't possible with ANSI colors.

    For more information on ANSI colors in Textual, see Why no Ansi Themes?.

    "},{"location":"FAQ/#how-do-i-center-a-widget-in-a-screen","title":"How do I center a widget in a screen?","text":"

    Tip

    See How To Center Things in the Textual documentation for a more comprensive answer to this question.

    To center a widget within a container use align. But remember that align works on the children of a container, it isn't something you use on the child you want centered.

    For example, here's an app that shows a Button in the middle of a Screen:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nclass ButtonApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"PUSH ME!\")\n\nif __name__ == \"__main__\":\n    ButtonApp().run()\n

    If you use the above on multiple widgets, you'll find they appear to \"left-align\" in the center of the screen, like this:

    +-----+\n|     |\n+-----+\n\n+---------+\n|         |\n+---------+\n\n+---------------+\n|               |\n+---------------+\n

    If you want them more like this:

         +-----+\n     |     |\n     +-----+\n\n   +---------+\n   |         |\n   +---------+\n\n+---------------+\n|               |\n+---------------+\n

    the best approach is to wrap each widget in a Center container that individually centers it. For example:

    from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Button\n\nclass ButtonApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Center(Button(\"PUSH ME!\"))\n        yield Center(Button(\"AND ME!\"))\n        yield Center(Button(\"ALSO PLEASE PUSH ME!\"))\n        yield Center(Button(\"HEY ME ALSO!!\"))\n\nif __name__ == \"__main__\":\n    ButtonApp().run()\n

    "},{"location":"FAQ/#how-do-i-fix-workerdeclarationerror","title":"How do I fix WorkerDeclarationError?","text":"

    Textual version 0.31.0 requires that you set thread=True on the @work decorator if you want to run a threaded worker.

    If you want a threaded worker, you would declare it in the following way:

    @work(thread=True)\ndef run_in_background():\n    ...\n

    If you don't want a threaded worker, you should make your work function async:

    @work()\nasync def run_in_background():\n    ...\n

    This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results.

    "},{"location":"FAQ/#how-do-i-pass-arguments-to-an-app","title":"How do I pass arguments to an app?","text":"

    When creating your App class, override __init__ as you would when inheriting normally. For example:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nclass Greetings(App[None]):\n\n    def __init__(self, greeting: str=\"Hello\", to_greet: str=\"World\") -> None:\n        self.greeting = greeting\n        self.to_greet = to_greet\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Static(f\"{self.greeting}, {self.to_greet}\")\n

    Then the app can be run, passing in various arguments; for example:

    # Running with default arguments.\nGreetings().run()\n\n# Running with a keyword argument.\nGreetings(to_greet=\"davep\").run()\n\n# Running with both positional arguments.\nGreetings(\"Well hello\", \"there\").run()\n

    "},{"location":"FAQ/#no-widget-called-textlog","title":"No widget called TextLog","text":"

    The TextLog widget was renamed to RichLog in Textual 0.32.0. You will need to replace all references to TextLog in your code, with RichLog. Most IDEs will have a search and replace function which will help you do this.

    Here's how you should import RichLog:

    from textual.widgets import RichLog\n

    "},{"location":"FAQ/#why-do-some-key-combinations-never-make-it-to-my-app","title":"Why do some key combinations never make it to my app?","text":"

    Textual can only ever support key combinations that are passed on by your terminal application. Which keys get passed on can differ from terminal to terminal, and from operating system to operating system.

    Because of this it's best to stick to key combinations that are known to be universally-supported; these include the likes of:

    • Letters
    • Numbers
    • Numbered function keys (especially F1 through F10)
    • Space
    • Return
    • Arrow, home, end and page keys
    • Control
    • Shift

    When creating bindings for your application we recommend picking keys and key combinations from the above.

    Keys that aren't normally passed through by terminals include Cmd and Option on macOS, and the Windows key on Windows.

    If you need to test what key combinations work in different environments you can try them out with textual keys.

    "},{"location":"FAQ/#why-doesnt-textual-look-good-on-macos","title":"Why doesn't Textual look good on macOS?","text":"

    You may find that the default macOS Terminal.app doesn't render Textual apps (and likely other TUIs) very well, particuarily when it comes to box characters. For instance, you may find it displays misaligned blocks and lines like this:

    You can (mostly) fix this by opening settings -> profiles > Text tab, and changing the font settings. We have found that Menlo Regular font, with a character spacing of 1 and line spacing of 0.805 produces reasonable results. If you want to use another font, you may have to tweak the line spacing until you get good results.

    With these changes, Textual apps render more as intended:

    Even with this fix, Terminal.app has a few limitations. It is limited to 256 colors, and can be a little slow compared to more modern alternatives. Fortunately there are a number of free terminal emulators for macOS which produces high quality results.

    We recommend any of the following terminals:

    • iTerm2
    • Kitty
    • WezTerm
    "},{"location":"FAQ/#terminalapp-colors","title":"Terminal.app colors","text":""},{"location":"FAQ/#iterm2-colors","title":"iTerm2 colors","text":""},{"location":"FAQ/#why-doesnt-textual-support-ansi-themes","title":"Why doesn't Textual support ANSI themes?","text":"

    Textual will not generate escape sequences for the 16 themeable ANSI colors.

    This is an intentional design decision we took for for the following reasons:

    • Not everyone has a carefully chosen ANSI color theme. Color combinations which may look fine on your system, may be unreadable on another machine. There is very little an app author or Textual can do to resolve this. Asking users to simply pick a better theme is not a good solution, since not all users will know how.
    • ANSI colors can't be manipulated in the way Textual can do with other colors. Textual can blend colors and produce light and dark shades from an original color, which is used to create more readable text and user interfaces. Color blending will also be used to power future accessibility features.

    Textual has a design system which guarantees apps will be readable on all platforms and terminals, and produces better results than ANSI colors.

    There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme.

    "},{"location":"FAQ/#why-doesnt-the-datatable-scroll-programmatically","title":"Why doesn't the DataTable scroll programmatically?","text":"

    If scrolling in your DataTable is apparently broken, it may be because your DataTable is using the default value of height: auto. This means that the table will be sized to fit its rows without scrolling, which may cause the container (typically the screen) to scroll. If you would like the table itself to scroll, set the height to something other than auto, like 100%.

    NOTE: As of Textual v0.31.0 the max-height of a DataTable is set to 100%, this will mean that the above is no longer the default experience.

    Generated by FAQtory

    "},{"location":"getting_started/","title":"Getting started","text":"

    All you need to get started building Textual apps.

    "},{"location":"getting_started/#requirements","title":"Requirements","text":"

    Textual requires Python 3.8 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.

    Your platform

    "},{"location":"getting_started/#linux-all-distros","title":"Linux (all distros)","text":"

    All Linux distros come with a terminal emulator that can run Textual apps.

    "},{"location":"getting_started/#macos","title":"macOS","text":"

    The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as iterm2, Kitty, or WezTerm.

    "},{"location":"getting_started/#windows","title":"Windows","text":"

    The new Windows Terminal runs Textual apps beautifully.

    "},{"location":"getting_started/#installation","title":"Installation","text":"

    Here's how to install Textual.

    "},{"location":"getting_started/#from-pypi","title":"From PyPI","text":"

    You can install Textual via PyPI, with the following command:

    pip install textual\n

    If you plan on developing Textual apps, you should also install textual developer tools:

    pip install textual-dev\n
    "},{"location":"getting_started/#from-conda-forge","title":"From conda-forge","text":"

    Textual is also available on conda-forge. The preferred package manager for conda-forge is currently micromamba:

    micromamba install -c conda-forge textual\n

    And for the textual developer tools:

    micromamba install -c conda-forge textual-dev\n
    "},{"location":"getting_started/#textual-cli","title":"Textual CLI","text":"

    If you installed the developer tools you should have access to the textual command. There are a number of sub-commands available which will aid you in building Textual apps. Run the following for a list of the available commands:

    textual --help\n

    See devtools for more about the textual command.

    "},{"location":"getting_started/#demo","title":"Demo","text":"

    Once you have Textual installed, run the following to get an impression of what it can do:

    python -m textual\n

    If Textual is installed you should see the following:

    Textual\u00a0Demo \u2b58Textual\u00a0Demo TOP\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Widgets\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Textual\u00a0widgets\u00a0are\u00a0powerful\u00a0interactive\u00a0components.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Widgets Build\u00a0your\u00a0own\u00a0or\u00a0use\u00a0the\u00a0builtin\u00a0widgets.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Input\u00a0Text\u00a0/\u00a0Password\u00a0input.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Rich\u00a0content\u00a0\u2022\u00a0Button\u00a0Clickable\u00a0button\u00a0with\u00a0a\u00a0number\u00a0of\u00a0styles.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Switch\u00a0A\u00a0switch\u00a0to\u00a0toggle\u00a0between\u00a0states.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2583\u2583 \u00a0\u2022\u00a0DataTable\u00a0A\u00a0spreadsheet-like\u00a0widget\u00a0for\u00a0navigating\u00a0data.\u00a0Cells\u00a0may\u00a0contain\u00a0text\u00a0or\u00a0Rich\u00a0 renderables.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 CSS\u00a0\u2022\u00a0Tree\u00a0An\u00a0generic\u00a0tree\u00a0with\u00a0expandable\u00a0nodes.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0DirectoryTree\u00a0A\u00a0tree\u00a0of\u00a0file\u00a0and\u00a0folders.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0...\u00a0many\u00a0more\u00a0planned\u00a0... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258eUsername\u258awill\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a\u2585\u2585 \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258ePassword\u258aPassword\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a \u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258a \u258eLogin\u258a \u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bar\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Baz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(0,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(1,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(2,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(3,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(4,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(5,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(6,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(7,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(8,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(9,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582 \u00a0Cell\u00a0(10,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(11,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(12,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(13,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258f \u00a0CTRL+B\u00a0\u00a0Sidebar\u00a0\u00a0CTRL+T\u00a0\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0CTRL+S\u00a0\u00a0Screenshot\u00a0\u00a0F1\u00a0\u00a0Notes\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    "},{"location":"getting_started/#examples","title":"Examples","text":"

    The Textual repository comes with a number of example apps. To try out the examples, first clone the Textual repository:

    HTTPSSSHGitHub CLI
    git clone https://github.com/Textualize/textual.git\n
    git clone git@github.com:Textualize/textual.git\n
    gh repo clone Textualize/textual\n

    With the repository cloned, navigate to the /examples/ directory where you will find a number of Python files you can run from the command line:

    cd textual/examples/\npython code_browser.py ../\n
    "},{"location":"getting_started/#need-help","title":"Need help?","text":"

    See the help page for how to get help with Textual, or to report bugs.

    "},{"location":"help/","title":"Help","text":"

    If you need help with any aspect of Textual, let us know! We would be happy to hear from you.

    "},{"location":"help/#bugs-and-feature-requests","title":"Bugs and feature requests","text":"

    Report bugs via GitHub on the Textual issues page. You can also post feature requests via GitHub issues, but see the roadmap first.

    "},{"location":"help/#help-with-using-textual","title":"Help with using Textual","text":"

    You can seek help with using Textual in the discussion area on GitHub.

    "},{"location":"help/#discord-server","title":"Discord Server","text":"

    For more realtime feedback or chat, join our discord server to connect with the Textual community.

    "},{"location":"roadmap/","title":"Roadmap","text":"

    We (textualize.io) are actively building and maintaining Textual.

    We have many new features in the pipeline. This page will keep track of that work.

    "},{"location":"roadmap/#features","title":"Features","text":"

    High-level features we plan on implementing.

    • Accessibility
      • Integration with screen readers
      • Monochrome mode
      • High contrast theme
      • Color-blind themes
    • Command palette
      • Fuzzy search
    • Configuration (.toml based extensible configuration format)
    • Console
    • Devtools
      • Integrated log
      • DOM tree view
      • REPL
    • Reactive state abstraction
    • Themes
      • Customize via config
      • Builtin theme editor
    "},{"location":"roadmap/#widgets","title":"Widgets","text":"

    Widgets are key to making user-friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.

    • Buttons
      • Error / warning variants
    • Color picker
    • Checkbox
    • Content switcher
    • DataTable
      • Cell select
      • Row / Column select
      • API to update cells / rows
      • Lazy loading API
    • Date picker
    • Drop-down menus
    • Form Widget
      • Serialization / Deserialization
      • Export to attrs objects
      • Export to PyDantic objects
    • Image support
      • Half block
      • Braille
      • Sixels, and other image extensions
    • Input
      • Validation
      • Error / warning states
      • Template types: IP address, physical units (weight, volume), currency, credit card etc
    • Select control (pull-down)
    • Markdown viewer
      • Collapsible sections
      • Custom widgets
    • Plots
      • bar chart
      • line chart
      • Candlestick chars
    • Progress bars
      • Style variants (solid, thin etc)
    • Radio boxes
    • Spark-lines
    • Switch
    • Tabs
    • TextArea (multi-line input)
      • Basic controls
      • Indentation guides
      • Smart features for various languages
      • Syntax highlighting
    "},{"location":"tutorial/","title":"Tutorial","text":"

    Welcome to the Textual Tutorial!

    By the end of this page you should have a solid understanding of app development with Textual.

    Quote

    If you want people to build things, make it fun.

    \u2014 Will McGugan (creator of Rich and Textual)

    "},{"location":"tutorial/#video-series","title":"Video series","text":"

    This tutorial has an accompanying video series which covers the same content.

    "},{"location":"tutorial/#stopwatch-application","title":"Stopwatch Application","text":"

    We're going to build a stopwatch application. This application should show a list of stopwatches with buttons to start, stop, and reset the stopwatches. We also want the user to be able to add and remove stopwatches as required.

    This will be a simple yet fully featured app \u2014 you could distribute this app if you wanted to!

    Here's what the finished app will look like:

    stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:16.20 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:12.16 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:08.10 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    "},{"location":"tutorial/#try-it-out","title":"Try it out!","text":"

    The following is not a screenshot, but a fully interactive Textual app running in your browser.

    Try in Textual-web

    Tip

    See textual-web if you are interested in publishing your Textual apps on the web.

    "},{"location":"tutorial/#get-the-code","title":"Get the code","text":"

    If you want to try the finished Stopwatch app and follow along with the code, first make sure you have Textual installed then check out the Textual repository:

    HTTPSSSHGitHub CLI
    git clone https://github.com/Textualize/textual.git\n
    git clone git@github.com:Textualize/textual.git\n
    gh repo clone Textualize/textual\n

    With the repository cloned, navigate to docs/examples/tutorial and run stopwatch.py.

    cd textual/docs/examples/tutorial\npython stopwatch.py\n
    "},{"location":"tutorial/#type-hints-in-brief","title":"Type hints (in brief)","text":"

    Tip

    Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.

    We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like mypy to catch bugs before your code runs.

    The following function contains type hints:

    def repeat(text: str, count: int) -> str:\n    \"\"\"Repeat a string a given number of times.\"\"\"\n    return text * count\n

    Parameter types follow a colon. So text: str indicates that text requires a string and count: int means that count requires an integer.

    Return types follow ->. So -> str: indicates this method returns a string.

    "},{"location":"tutorial/#the-app-class","title":"The App class","text":"

    The first step in building a Textual app is to import and extend the App class. Here's a basic app class we will use as a starting point for the stopwatch app.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    If you run this code, you should see something like the following:

    stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Hit the D key to toggle between light and dark mode.

    stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Hit Ctrl+C to exit the app and return to the command prompt.

    "},{"location":"tutorial/#a-closer-look-at-the-app-class","title":"A closer look at the App class","text":"

    Let's examine stopwatch01.py in more detail.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The first line imports the Textual App class, which we will use as the base class for our App. The second line imports two builtin widgets: Footer which shows a bar at the bottom of the screen with bound keys, and Header which shows a title at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.

    The following lines define the app itself:

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more.

    Here's what the above app defines:

    • BINDINGS is a list of tuples that maps (or binds) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the D key on to the \"toggle_dark\" action. See key bindings in the guide for details.

    • compose() is where we construct a user interface with widgets. The compose() method may return a list of widgets, but it is generally easier to yield them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. Header() and Footer().

    • action_toggle_dark() defines an action method. Actions are methods beginning with action_ followed by the name of the action. The BINDINGS list above tells Textual to run this action when the user hits the D key. See actions in the guide for details.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The final three lines create an instance of the app and calls the run() method which puts your terminal in to application mode and runs the app until you exit with Ctrl+C. This happens within a __name__ == \"__main__\" block so we could run the app with python stopwatch01.py or import it as part of a larger project.

    "},{"location":"tutorial/#designing-a-ui-with-widgets","title":"Designing a UI with widgets","text":"

    Textual comes with a number of builtin widgets, like Header and Footer, which are versatile and re-usable. We will need to build some custom widgets for the stopwatch. Before we dive in to that, let's first sketch a design for the app \u2014 so we know what we're aiming for.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVcXGlP40pcdTAwMTb93r9cdTAwMDIxX2akxq/2paXRqFx1MDAwM4R9XHUwMDBiW8PMXHUwMDEzMrFDXGbxguNA4Kn/+5SddOwktuM4S/tFLVx1MDAxYVxcjuu66tx77lJVf33Z2NhcZj48c/PbxqbZb+pcdTAwMWTL8PX3za/h9TfT71quo5pQ9HfX7fnN6M52XHUwMDEweN1vf/xh6/6LXHUwMDE5eFx1MDAxZL1pam9Wt6d3ukHPsFxcrenaf1iBaXf/XHUwMDEz/jzVbfPfnmtcdTAwMWKBr8WdbJmGXHUwMDE1uP6gL7Nj2qZcdTAwMTN01dP/q/7e2Pgr+pmQzjebge48dczoXHUwMDBiUVMsIFx1MDAwNmzy6qnrRMJcbi5cdTAwMDWFXGaR0VxyVndHdVx1MDAxN5iGam0pkc24Jby0+aizxo/G/VPr6uX83Hu/ueqSPo97bVmdzmXw0Ymk6rrqZeK2buC7L+atZVx1MDAwNG3VXG4nrmd9y3d7T23H7HbHvuN6etNcbj7Ca1x1MDAwMIyuXHUwMDBlhuDbRnylXHUwMDFmTlx1MDAxMCdcdTAwMWFkXHUwMDAwUoboqCH8KpJcXCOEYpi4Plx1MDAxMGbb7ajBV8L8XHUwMDAzRJ9YnEe9+fKkZHKM+Fx1MDAxZVx1MDAwMpqAJlx1MDAwNuF9+IqqQ1xyy4lcdTAwMGXapvXUXHUwMDBlXHUwMDA2gmtcdTAwMWNcbp7o24yGXHUwMDFkXCLEOUFcYqNRS9ijd2BEXGL4c3Lg2rrvXHJcdTAwMDdos1x1MDAxYv6RkDZcdTAwMTR0d1x1MDAxMj5JXGIlpvag2/C2m5+HRzdcdTAwMTf1t7uL8114vnc4etZcdTAwMTjedN933zdHLT+Hv8Wi9TxDXHUwMDFmgFxiMlx1MDAwNiRcdTAwMTaSXHUwMDEwgmNcdTAwMWN2LOdFNTq9Tie+5jZfYtxFV39+LYF3gmBcdTAwMTbeJVx1MDAxNYJQSorj3XrEnty37P7JsSlcdTAwMWT97NV7/VGvON5cdTAwMDXRXHUwMDA0wXxcdTAwMWPsXHUwMDE4YlxyYyqT18uAvaVTRNE02JWGTWOcsSlwc8FcdTAwMDRUUCDrXHUwMDAy9y/MXHUwMDA0Zj9cdTAwMThH82CG67KPb+3+/bFcclx1MDAwZnZunFx1MDAwN3v/sL01XHUwMDE3tlx1MDAxOVx1MDAwNVxigWVhe0zOYmZcdTAwMWMqXHUwMDExXHUwMDE4R1x1MDAxY4nCuE5/63RcXLf1Zrvnm1VAtkxDNlZ4X1x1MDAxY9mBrztdT/dcdTAwMTWaUtBNU9CN8LTpJlAqky9WgO5lXHUwMDAyMJ5n11x0Lq3PcKhcdTAwMTNcdTAwMGZcYq/WddvqfIxNVYRMJell4CZcdTAwMDXVu6bqMcIhXHUwMDFmu/d7x3pcbpG72VTvYPpjoFx1MDAwZSzl6oxusC3DSFx1MDAxYfOmXHUwMDEyQFfP9Fx1MDAwZopcdTAwMThh17eeLEfvXFwl5SvPXHUwMDFmjMlM/lAug0SCo8J65lx1MDAxY2/3t06R6Fx1MDAxY8BG42brXHUwMDAzXFw1/I9q81x1MDAwN2NEXHUwMDAzWIJJd4lgpPRcZoGF/aWmaVx1MDAxMEMvSiFiSskgR8q1XHUwMDEyZG1cdTAwMTQymMpeXHUwMDFmn+zsideL9zuj9tK6sE9/bDfS/aNIU2Jcbvma/thZzJTeYXFmXHUwMDEyWPmXWMaTtVwiZqJcdOM4yUycXHUwMDAzyVxiT8zsLI3JXHUwMDFm5ooyk7JcdTAwMTnpOkOp8rpcdTAwMTbXmaWQXHUwMDEzXHUwMDA3XGIoXHUwMDFkS7jhqyenXHUwMDEyXHUwMDE4XFyMnFx1MDAxYWbXXGbWyk4zTPwkO1xyXHUwMDA0LE9PMGFcdTAwMTEntI1wXGKQXHUwMDE0rHh4c/zs9smja183ru5Ojp73jWtcdTAwMTI0q01PhCtVo2Lc34v8QMQ1oMhhPKYuXHUwMDEz4kSfND3DXHUwMDFhnExcdTAwMTiMXHUwMDE0XHUwMDBlU02OO6dDveMq5FRcdTAwMTaArkDtyvHKx0n/ljx8Xlx1MDAwN96xW/NZv+18st1cbkY8kmRcdTAwMDFcdTAwMWQyjpVZ4ax4wJP+0lx1MDAxNadcdTAwMTVcdTAwMTVIZGBd+WJ0knCWzitcdTAwMDRPwzyFV6hcdTAwMDQq5GGr8MeqXHUwMDEz9Fx1MDAwMPAt/Mc1XHUwMDA018ouMyz0JLskxSzPMVhmptCgQFxicVxuOSusende//LkZGf39uiqVoeg3Ttv+8e/k2RwkZQxXHUwMDA1krOpnDGBQmOccrGqIIgypnHO8Vx1MDAxOJOMJY1cdTAwMDVcdTAwMWRPZY/CXCJcdTAwMDRcdTAwMTX/XHUwMDEwsVYtXHUwMDE0XHUwMDEwYonm0MLyoKRcdTAwMTRngpJcdTAwMTPJkVx1MDAwNKQ4XHUwMDFmyGOhO/unXHKf1M97bbttXl1cdTAwMWZUvZAhqFx1MDAwNlx1MDAwNaWEXHQ+XHUwMDE5mlx1MDAxM00uI7ubVcoomt1ccrP8gvLEq6wlNP++3Xpr2O32XHUwMDE2Mz6fejY4erg74uOuz3JD8/RcdTAwMGWr50IpK5KlM0x5XHUwMDBmXHUwMDAwwDlcXKj8Ua6oXHUwMDBipaxCltJQppElKM1yYnMsXHUwMDAwXHUwMDEwfFx1MDAxNTW/6vhQl4Hurzc2n2HlpzPHoYBcdTAwMGL4TSiborCQiOE5XHUwMDEyYW7t5NW8QPzItnuNeqv/dnyB33+vuvFcIsE5XHUwMDAxqZkwXCKVT1xuVqxspVx1MDAwMnRKwuhFrrve/iDfL8+O/Ju9k7bVMPZcdTAwMGZcdTAwMDS+P2uvkrTSO6xcdTAwMWVpIZpZgFGBP8CcUFA8xZU/zFx1MDAxNVUjXHUwMDE192eoXHUwMDExpVx1MDAxYV9xOrlY2C9cdFx1MDAxMpJSuM5s8lx1MDAxMIFwXHUwMDBlXHUwMDA0LsZYw3BcdTAwMWFo41x1MDAwM7py3pph+zOi/kjM8uyFWKazKFx1MDAxOEZcdTAwMDBcdTAwMTFQ3Fl8RkivfaKzXHUwMDFh3vGI34JXd82b/d9cdTAwMTlfkdlBP9Mog1x1MDAxNOGpJVx1MDAwNsroaVx1MDAwYkf8Lf1cdTAwMTFcdTAwMDCaXHUwMDE28XNccipdXHUwMDFh0/WR4il9R1igsPeoXHUwMDBmOKmHXGJcdTAwMTBlXHUwMDEwMV1fOXRcdTAwMTbP6PpcdTAwMTXbu7o/OFx1MDAwMSe39nZw+eRcdTAwMWScXs63WkxIXHRji7NcIp6BIDvBLJByJOZYXHUwMDE3mf7O6XBv+m63u9XWg2b794NewEzQh1x1MDAwZVx1MDAxM4aMrDTJTCmfXHUwMDA2fUqAxCRcdTAwMDJUKcl6k8xzXHUwMDAzcTG62Td1I0lcdTAwMWJrYJpcdTAwMTl2epJphlx1MDAxMpYnXHUwMDE5XG4zSVx1MDAwNlwiyKOca2Gtc+y3k8tcdTAwMTf48Irk+71OwFx1MDAxYjx9LpVZRkvSNzpb34CGXGKcjIRcIq9cdTAwMTAqfWNkXCJEKUE0XGKJR5OlXHUwMDExXHKZVjWcsohcciNcdTAwMTLSUWVcYuU2QPrdzsOWj/pta5/dP+3hXHUwMDA3ez5CkYrd4/dZVeDCM0vzkGEogUCgePyf/ta/nVJcbkCcZkJcXPlRy4D4XGZGSYF5SvxcdTAwMTKuhyFcdTAwMDCtmVDmXHUwMDA14mKEUnfdYM2EMsMmT1x1MDAxMspQwkVcYiUn56ZAiOZSujP60NxvdY1cdTAwMWYt1Kyf39qn/aB5U/FaJdOYXHUwMDEwJKVYSTHXplLf1ShWXCLMJcOYraBamc4xw1x1MDAxMsZcdTAwMWU7O3uxrf7lNjk6291BXHUwMDFl+Vx1MDAxMOk5t1x1MDAxMntcXFx1MDAxMGF8PbVQxrM3XHUwMDAzIE64ilx1MDAwN+fINNvPp/a2d3x0dF4zOvt35lPt8sWpei2Ua5hcdTAwMTE0UYn/XHUwMDE42H9ccirVXzhgX7RcdTAwMTgqiFRTQcSai6E7XHI9eDyy2s9cdTAwMTdcdTAwMTf711x1MDAwZl3y2q5jP1x1MDAxZOPF8soreuwsry+9w3lcdTAwMTRS4DCHumqvj+akq7lcdTAwMDBcdTAwMTBcdFx1MDAwNouHNPnDXFzZXCIrytRGTjW2XHUwMDA0bVxcSpVcdTAwMTVcYkokoGtdXHUwMDAxXVx1MDAwMoaL+Xzrr7LO4I9lV1lcdMkmP4wogpTC4jt0asfXzkO7Z+yC163vV/XWNdjln5WvXHUwMDBmQY1yXHRTNjVTJrRVr1x1MDAwYi1XZoWIXHUwMDAxiClfRVxuL4+4LpqH6PFwz3Vfnlxm9/Ni79Opi93F+XDpj53Fh+lcdTAwMWRWj1x1MDAwZrHIXFw8ijBcdTAwMDVcXMGmeFx1MDAxOSl/lKuqnSxTOznRhFgxXHUwMDE5XHUwMDE2q98qXHUwMDE2RGqq1rxVdc1cXPi76rczSKV0/TYz84hpls5cdTAwMTGBJZtrb3jt7HnLe39/dPmLrd95dYs9YiND59aSVZ/tgEpGNSogSFnmR2G4LUjmXHUwMDFm84FcdNZJ2W2rXHUwMDAwpejbXHUwMDE0/SGiXGaCSM5D1TNcdTAwMWW59Cdr37dcdTAwMGbPXHUwMDFmxPfDmjh8faa017r/sbRMXG6lXHUwMDAwr6/sXHUwMDE2blx1MDAxOH9cdTAwMWbPnSdthFx1MDAxOPvCyFx1MDAwNHTMVpBjIcY2yY+bh7FcdTAwMTdJ270+XHUwMDEwJtdcdTAwMTJcZlx1MDAwNjWNfrOPP1FcdTAwMDAkmPB5zvvJn+Zq2lx1MDAwMlxuNFxmXHUwMDA1Y1x1MDAwMmMsMJw0XHUwMDA3QoOYhVx1MDAwYiZouFtyNTaBizBcdTAwMDWFZbhcdTAwMWSR8OSBXHUwMDAx8cJcdTAwMGWoQYSEomGGpVx1MDAwMvykvVAuNCfhSq/57UUkZNn6hGCiVH2iXHUwMDFiRnU1yzEs50k1xibj1/lVXHUwMDA3XHUwMDA1yCXS1WYvlHJcdTAwMGJoTKrRXHUwMDEzMFxc8IeT5yGFY6F7USSiodCLYThcdTAwMWNGQeTwhpHp2jRcdTAwMWQjlmn8NfRusO3atlx1MDAxNahcdTAwMDE4dy0nmLwjeqPvoZ61TX1Kb9WTk22TXG7phU9cdTAwMWO3zvFvXHUwMDFiMWSjP0a///k19e6tTDxFrZNQilx1MDAxZvcl+f/cplx1MDAwNKHszJZEUir9wsUzW/lMVFFTwjV1V9pcdTAwMGVMisP1zFxmQ45BaFNXYkaksmScyF9cdTAwMWaeslSGKFx0XHUwMDE5XHUwMDAyXHUwMDEw8TCyXHUwMDAyaOq4XGbVICVgskTOa1x1MDAxMTtCldqW8vOXbUeApiw9UapcdTAwMDEg41xckZ9I3DQwI0IjXHUwMDAyRJ5jvv3IkiW/aDgmXHUwMDBilzzccqIgxaREdFpcdTAwMTaENUSHO1x1MDAwZqek+VvZrEzwhp8p2M5pszKTXHUwMDBmXHS+nNzwhChnhOHiqcGjz7u911Zw8456jkktv9WA9duKWywsNJZusZQnpFx0wfLPR1gsXHUwMDEwimWO3ZxYvtFSPlx1MDAwZVx1MDAxMFwiZM1lsVx1MDAwNVx1MDAwMpbcQGhVXHUwMDAx1mqPY1x1MDAxNJSBeUrVS1xusP7n/DOyUKbxr9RYK5G6mi9cdTAwMWazcLSVlKyct4SzXHUwMDE3okjCwpNw5ti1kj/7XHUwMDE1NT1UxV2/llx1MDAxMidXqlx1MDAwZk6skJpiWIakcth5cqf2Mk1cdTAwMTBcdTAwMTOaXHUwMDE4nLhcdTAwMWFcdTAwMTGPiD2QODOjUVxuOFx1MDAwZuXkTFwiknagXHUwMDA1XHUwMDA2UGBaJlVT5dArn87G3Fx1MDAxNFxiiVQ2XHUwMDFhcCYoJ3FcYj1yU5SXMtixkO8x/X0jrkwkhZ9pXGbN6b7k1jd5zuJcdTAwMWWoaF1IJIpnc9+E3fQ+3G6999yRXHUwMDA3dv2cg+1SlmSdp6lRXHJcdTAwMDCkQszpbC5ccjclrGg5W9FcdTAwMDNcYjFV1mE1S0fzPILT21OvfaVcdTAwMDb70rm+wVcnwdbxoZ3uXHUwMDExzFPIXFz6Y2dcdTAwMTUy0zss7r4oJlxy/cd5dlx1MDAwMeYqY1YskdyJMJX+4FhKguY4ai1/mCu6sEe56pmayFXYXFyBo1x1MDAxM1x1MDAxMMRcZofnXHUwMDExrfPgm1x1MDAxMlx1MDAxMFxczINe/7GGM3gj71jDL0Pl3dQ97zJQ4zZyStTUWMbw5eOx2nyzzPda9kl8X4a6XHUwMDFiKolcdTAwMTlOzF8/v/z8P1xiXCJcdTAwMWT6In0= StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset"},{"location":"tutorial/#custom-widgets","title":"Custom widgets","text":"

    We need a Stopwatch widget composed of the following child widgets:

    • A \"Start\" button
    • A \"Stop\" button
    • A \"Reset\" button
    • A time display

    Textual has a builtin Button widget which takes care of the first three components. All we need to build is the time display widget which will show the elapsed time and the stopwatch widget itself.

    Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.

    stopwatch02.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    We've imported two new widgets in this code: Button, which creates a clickable button, and Static which is a base class for a simple control. We've also imported ScrollableContainer from textual.containers which (as the name suggests) is a Widget which contains other widgets.

    We've defined an empty TimeDisplay widget by extending Static. We will flesh this out later.

    The Stopwatch widget class also extends Static. This class has a compose() method which yields child widgets, consisting of three Button objects and a single TimeDisplay object. These widgets will form the stopwatch in our sketch.

    "},{"location":"tutorial/#the-buttons","title":"The buttons","text":"

    The Button constructor takes a label to be displayed in the button (\"Start\", \"Stop\", or \"Reset\"). Additionally, some of the buttons set the following parameters:

    • id is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
    • variant is a string which selects a default style. The \"success\" variant makes the button green, and the \"error\" variant makes it red.
    "},{"location":"tutorial/#composing-the-widgets","title":"Composing the widgets","text":"

    To add widgets to our application we first need to yield them from the app's compose() method:

    The new line in StopwatchApp.compose() yields a single ScrollableContainer object which will create a scrolling list of stopwatches. When classes contain other widgets (like ScrollableContainer) they will typically accept their child widgets as positional arguments. We want to start the app with three stopwatches, so we construct three Stopwatch instances and pass them to the container's constructor.

    "},{"location":"tutorial/#the-unstyled-app","title":"The unstyled app","text":"

    Let's see what happens when we run stopwatch02.py.

    stopwatch02.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2586\u2586 Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any styles to our new widgets.

    "},{"location":"tutorial/#writing-textual-css","title":"Writing Textual CSS","text":"

    Every widget has a styles object with a number of attributes that impact how the widget will appear. Here's how you might set white text and a blue background for a widget:

    self.styles.background = \"blue\"\nself.styles.color = \"white\"\n

    While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.

    Info

    The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn.

    CSS makes it easy to iterate on the design of your app and enables live-editing \u2014 you can edit CSS and see the changes without restarting the app!

    Let's add a CSS file to our application.

    stopwatch03.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch03.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Adding the CSS_PATH class variable tells Textual to load the following file when the app starts:

    stopwatch03.tcss
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n\nTimeDisplay {\n    content-align: center middle;\n    text-opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n

    If we run the app now, it will look very different.

    stopwatch03.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    This app looks much more like our sketch. Let's look at how Textual uses stopwatch03.tcss to apply styles.

    "},{"location":"tutorial/#css-basics","title":"CSS basics","text":"

    CSS files contain a number of declaration blocks. Here's the first such block from stopwatch03.tcss again:

    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n

    The first line tells Textual that the styles should apply to the Stopwatch widget. The lines between the curly brackets contain the styles themselves.

    Here's how this CSS code changes how the Stopwatch widget is displayed.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT20pcdTAwMTb9nl/hYuZj3Ol9SdXUXHUwMDE0mLCEsIUlycx7lVx1MDAxMpKwXHUwMDE1ZMuxZFx1MDAxNr/Kf58rQazd2MZcdTAwMDYz9VRcdTAwMTRcdTAwMTi1lqu+5/Q993bLf71pNNaiu7679r6x5t7alu85XHUwMDAz62btbbz/2lx1MDAxZIRe0IMmmvxcdTAwMWZcdTAwMDbDgZ1cdTAwMWPZiaJ++P7du641uHKjvm/ZLrr2wqHlh9HQ8Vx1MDAwMmRcdTAwMDfdd17kdsN/x79cdTAwMGasrvuvftB1olx1MDAwMUpv0nRcdTAwMWQvXG5cdTAwMDb393J9t+v2olx1MDAxMK7+X/i/0fgr+Z2xbuDakdVr+25yQtKUXHUwMDFhKDUt7j1cYnqJsUxcdTAwMWLKJNVkfIBcdTAwMTduwu1cIteB1ksw2U1b4l1rp+fnV527b8cnrfbt94D7XHUwMDFi3uczkt710vP9k+jOT6xcblx1MDAwM3iYtC2MXHUwMDA2wZX7xXOiXHUwMDBltJLC/rqzXHUwMDA2wbDd6blhmDsn6Fu2XHUwMDE33cWPgMc773vgfSPdc1x1MDAxYvvHMIQ1M5ozJcYt8amUcyR1Zue9Ja3Ah55cdTAwMDdL/oGTLbXlwrKv2mBQz0mPsV2HO1Z6zM3D81x0KZFSiuXu2nG9dieKn1x1MDAwNGOkXHUwMDA1XHUwMDExkmbu7ia9ToRmUnKJ06eN79nfdVx1MDAxMlx1MDAwMPyZ7Zme89AzvaHvp2bGXHJcdTAwMWaKoMlcdTAwMDIn41Duj1pHN44z3Fx1MDAxMteYbOtmv+tvjVx1MDAxZieHMmswXGJu1sYtv1x1MDAxZT6lXHUwMDE2XHL7jnVcdTAwMGZcdTAwMWRcIiU2WmAhXHUwMDE11eN23+tdXHUwMDE1jfVcdTAwMDP7KkVbsvfX2zlQrjNAKKCcXHUwMDFiXGZcdTAwMDBQxkyN8rsvx1x1MDAxZlpmt7l7sVx1MDAxZH36XHUwMDFlXHUwMDA1m+vfv929JMpcdTAwMDEvj8CcYYaUMErmMJXAXFxiZLhghD5cculcdTAwMWPbWKgy0onEZYBLWcJ1XGZcYqYwXHUwMDExz4LrT+vOZcft2ups+1x1MDAwN93fOT883j2+rcZ15N5GL1xi6+T2XHUwMDE1iFaE1CFagT9h5FJ8akRP7o48ojuW3Vx1MDAxOVx1MDAwZdxcdTAwMTXAtOCIVmNaU8SfjuloYPXCvjVcdTAwMDBAVYzgplxma8pKsNaEY8IlN1x1MDAwYoN1NfI0JopzXHUwMDA2XHUwMDAxxcyAvNTDQS868UZxJ1Oc27tldT3/LuekXHUwMDA0ktA/J5E1iLI9XHUwMDE4unDL+Fx1MDAxYUTlXHUwMDBlXve9dozZNVx1MDAxYlx1MDAxZcJcdTAwMWTk4Fx1MDAxY3mgasZcdTAwMDd0PcfJjuA2WGDBNVx1MDAwN7vTjLzBwGt7Pcs/zVx1MDAxOTh/1JC4NmpcdTAwMDA8qVx1MDAxNJRjNTXJLs9bR82D/Vx1MDAxZnZcdTAwMTieXvMvrattdui9LMnUY1x1MDAxY+NGIaNAbpiSPFJcdTAwMDJRSvPkWzTJiGCIXHUwMDE0mTymXHUwMDFiXHUwMDEzqKDbXHUwMDFlWCdcdTAwMDWEXHUwMDEyLtXiSDcplvxs/Tw7bNODjf6BXGZcdTAwMGZYf32fd9ZfVSxcdTAwMTGM1eFcXCjCmOQzxJLJ3bGaMFx1MDAxN1x1MDAwNNfBXFxcdTAwMTOkXGIvwGzRMKeqjO6KYIKxXHUwMDExhDC6OO3/SDDheFx1MDAwNug9LZhg/D75QfmOXFx6SHlkVC6GlKyZ81x1MDAwN1x1MDAxNsNqxZvkXFxRo9X06VxiiUbHwaXeXHUwMDEyx9d476K5c9lum9FqpyNcdTAwMTJYVZlzS4qUNE/PRury7upsRJeYJsBcYmJohoPLjCBk3TlmXHUwMDAz22/rI4b90Z41+mzvT1x1MDAxNUHeTrrsK0re61wik8a6jijGKEO1Znp6okzs5lx1MDAxNc1yXHUwMDE0q6GKXHUwMDA2/bVcdTAwMDCqTFxmTFVsqYhLRIPWXHUwMDEykv8/xqXPbug+b5LzyHhejEj3XHUwMDA2zsUurmrDkCaKMqxcdJ6aXba5acqbj6PNnVx1MDAxZGNv9Jtfty82/VVnl2FcdTAwMDblKXTPLYPMMlx1MDAwNZ+syGbKtV5cbj4gYpFBaHVoJVx1MDAxYTGLwmpi6TmJXHUwMDE1XHUwMDA1/TpW5Vx1MDAxZaRIod/GTCTRfYisYpFcdTAwMTF1LDKGXG4lKZ9ezIU6tH7Y/fObs30pdLBcdTAwMWZcdTAwMWVccjtm5VlEXGZSxVpAQiRjXHUwMDEwgU4gT5xHmUgmjLjGPDdVMyZcdTAwMTU3SFx1MDAwYpFv/M0uzDCnRlxuNVx1MDAwM71SlfVcdTAwMWJcdTAwMTf0Yc+vWVk3n4rK9KI1iDa8nuP12sVT3J5T0+JbYdRcbrpdL1x1MDAwMjOOXHUwMDAyr1x1MDAxN1x1MDAxNY9IrrtcdTAwMWVjveNaJebAlbNtRVL04yvm5W76qZGiJvln/PnPt5VHN8uOTXZnfJpe4k3278xcdTAwMTRcdTAwMTZEXHUwMDE2945cdTAwMGJ9XHUwMDFjc1x1MDAwZVx1MDAxNJ9+XHUwMDEyNNjb/EG2etf+9o9j94BcdTAwMGbZXHUwMDA3vi9Xn8JcdTAwMTKp4pzj/VxmqkHw/MulMEFwXHUwMDAzrFx1MDAxNIa7S1xuv1N3pFTGSFx1MDAxMlx1MDAwM4iQ4H2IjCVcdTAwMTXKXHUwMDE4xEqhZyq1/83n5+NzvZPjrejeXHUwMDE5qZ1Ih1xuZoO2q2W2XHUwMDExJp5Mz1x1MDAxND9cdTAwMWaj9lx1MDAwNiNcdTAwMWJH+ESwy+1u6+Y/ncNR3/+48tRcdTAwMDZcdTAwMDZcdTAwMGLMXGYuRWcuXHUwMDExlpLjZZbwKYabK11ZwIemglnjSifXXHUwMDE0XHUwMDAw8vJcXJ5rNm0luJxrWyiRyy5NTvvtzFx1MDAwNTGXitpJXHSipJEwWkwvq+Uu3zsyh7fru4e7QeeL+DB0jtXqXHUwMDEzVyFVZEhcdTAwMTKTXHUwMDA1RphcdTAwMTZWXHUwMDBmLZy4ulxcX0qZW2IsXHUwMDAxLa20kS+upv9mbPnoXG5fXHUwMDE2zpuKqlx1MDAxM+c0uObFveNcdTAwMDUpkFx1MDAwNzKq9fRz5XpcdTAwMDN3j/btT9s3XHUwMDE3n4eR/rBtt1uXXHUwMDBi5qtjhVx1MDAxZHehhKVKIa6q8mAqXHUwMDEwK+Soi6/VUiSK0TxdUshcZsJJ0sRNsqW+eGCwwdhcdTAwMTBN8PMswFx1MDAxYfV1Z7RPvu58O1xuNvpdtqPWeVrkzKGuODfxdtJ1N7+FV2fOQXvnzmtcdTAwMWXuaOc6PO21XHUwMDE2Oucxy/gykU11VVmKa4lcdTAwMDTeoVxcSZmpOD3GpD1ztH973lx1MDAxYlx1MDAxZFx1MDAwN1tcdTAwMTY+3W3tnUaOs/pMMlx1MDAxYVx1MDAxOVMxXHUwMDFmXHUwMDBmXGJFy5asRFat7SqLVCmwoYw/05LFJzDmcWRcdTAwMTOuNGczIDtcdTAwMDXQPNXfTjDwRnF91m/41l0wrJlgqalcdTAwMDP77mWeOIupXHUwMDAyl42aSN/aYlx1MDAxMnRnXHUwMDFkf6VcdTAwMTJcdTAwMWNcdTAwMGJNplx1MDAwZoSTvb6iwpVcdTAwMWKGaHHRTHKqXHUwMDExyFx1MDAxMEKZXHUwMDAwclPOl8hhXCKRllx1MDAxNFBccsZcdTAwMDDCVVxu7kz6yVxihERcdTAwMDKySGtwSibbTFclXHUwMDBiaJJslpnNRata+EPFXFwzM1x1MDAwNVWbMezhpZHdKcRWwmZ7XHUwMDE4W9kkiEOPXHUwMDE4JrHmMZS1zlx1MDAxY9W2+snIjZhhXFxcdTAwMTCBwftClFx1MDAxZT0np59sXHUwMDEyQUxjqZigQihcdTAwMDXAqrZIYKyEhFxcimNOy+54VWW0WmTHW7NcZupcdTAwMTlFfq0sMfVcdTAwMGLOMSdEsVx1MDAxOfLxw+DO4k54ff1ldHeuw7NbbM72Vn1Yo0IjTngpXHUwMDFk58QgoYtLUVx1MDAxNz6gVa1cdTAwMTEsi1x1MDAxMiaJxmyRazFej9x+mighXHLb9f1G11x1MDAxYYAsWFx1MDAwNUGSN2g+MVwiMolegbWUKqownaH8PdnbK8paRlx1MDAxOeKCK+CEXHUwMDAxksr0ce+5S5ApTlx1MDAxZi/8XVx1MDAxMYyIVFxuXHUwMDEzXHUwMDA16TdE81xuLcI5wsVcdTAwMWH9mNLUyDjZmGVcdTAwMWTIypbW6lwi/uSIkFx1MDAxNSFcdTAwMThRKiBcdTAwMDVUgsVcdTAwMDE9M3c7XHUwMDBl+Vxu3aeOS1x1MDAxNlx1MDAxZnGsNYZwJjVcdTAwMDX8UJa+XHIyNoUhSVx1MDAxNNFcdTAwMTW2vCbRUYvgeGtmwLsgscFVffGfU1x1MDAxMtdcdTAwMTPN9Fx1MDAwYj+DT/3t06/d1sneh83RlzY3gm0uupq4+IWfXFwhSD1UbuI9XHUwMDE5skDQXHUwMDEyTJb84k06bTpcdTAwMWWjMmsgxiVcdTAwMTCl43l38zxqY1lcdTAwMGKa01x1MDAwNXDpczyT2uhbTjxcdTAwMWH90bOSLmpcXFxmoyjoVa+Ly1x1MDAxNGieZ13cIzbOp0iMqZ+Rh5QyjjczvFQ3XHUwMDE5XHUwMDEyK0ptjTWIXHUwMDBlI0g8y22yq3DvJYlGy13WTeM8XHUwMDFlayZULHziZVx1MDAxMGWySzBcdTAwMTGyXGKBQVx1MDAxOUllZEX5XHUwMDEz0ktqlHjR2lxi5UYuYr1NnVx1MDAxNphcdTAwMWM68rJcdTAwMDRzoaRkQlx1MDAwYkY1XHUwMDE1qVvHYoBgXHUwMDA0fVx1MDAxZft2PmUy+fs18taA3OVKa2ygg1x1MDAxNElfXHUwMDBiXHUwMDFlXHUwMDFiXHUwMDAzQZvGqVwieJFRzMTrLos060GdNJfwvCClQln9bI2iWlxiQXG6mu2x4cy/utz+aX8ynYNd7Xe3xGZw5lxmVn0441qh4mqEZCBjXHUwMDE0SUrwUpfSXHUwMDEzOZVS0VhcdTAwMTlccu5/nlx1MDAxN7q+hV/7srP58/bC73V+3uy3XHUwMDA33Y81Xy8xh1Dh0NnPJ1RShzTs2CN/9Lyw8c+LIFxiq2dtnl2lTDJwPolCSP1SXHUwMDA2oWFw4Wz696EnY2FFKS2kQFLG31x1MDAwYsMwXHUwMDAxzFx1MDAxNTNcdTAwMTCOlvviv0RExyNcdTAwMDcxXHUwMDFhXCKpIFx1MDAxNStcdTAwMWGMRIWv/lx1MDAxOL+8aSBlXHUwMDAyXHUwMDEx+ULC5IkknVKYTI5cdTAwMTRcdTAwMDVhXCJcdTAwMTTTWihKuMBgXHUwMDFlz1x1MDAxY/a7YFx1MDAwMsExKWBPXHUwMDE2Jq928XEtoOKtOcZSnSh483DhNavfP4nA1WPPXHUwMDAwtjznYVBNn27t2nNvNipeTL5MtnhcXEp6OKa/m+Dv15tf/1x1MDAwM1x1MDAxY9s3yyJ9 Start00:00:00.00Reset5 lineshorizontal layout1 cell margin1 cell paddingaround buttonsbackground coloris $boost
    • layout: horizontal aligns child widgets horizontally from left to right.
    • background: $boost sets the background color to $boost. The $ prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as \"blue\" or rgb(20,46,210).
    • height: 5 sets the height of our widget to 5 lines of text.
    • margin: 1 sets a margin of 1 cell around the Stopwatch widget to create a little space between widgets in the list.
    • min-width: 50 sets the minimum width of our widget to 50 cells.
    • padding: 1 sets a padding of 1 cell around the child widgets.

    Here's the rest of stopwatch03.tcss which contains further declaration blocks:

    TimeDisplay {\n    content-align: center middle;\n    opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n

    The TimeDisplay block aligns text to the center (content-align), fades it slightly (opacity), and sets its height (height) to 3 lines.

    The Button block sets the width (width) of buttons to 16 cells (character widths).

    The last 3 blocks have a slightly different format. When the declaration begins with a # then the styles will be applied to widgets with a matching \"id\" attribute. We've set an ID on the Button widgets we yielded in compose. For instance the first button has id=\"start\" which matches #start in the CSS.

    The buttons have a dock style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.

    You may have noticed that the stop button (#stop in the CSS) has display: none;. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is not running. Similarly, we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.

    "},{"location":"tutorial/#dynamic-css","title":"Dynamic CSS","text":"

    We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a started state with a Stop button. When a stopwatch is started it should also have a green background and bold text.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa1PiSFx1MDAxNP3ur7CYr5rp98OqrS1fOO4oXCLq6ri1ZYWkIZFAMFx0XHUwMDAzOOV/305wJYFEXHUwMDEwXHUwMDExXHUwMDE5Synp7vS96b6nz+nb/Wtjc7NcdTAwMTRccruqtLNZUlx1MDAwM8v0XFw7MPulrbj8p1xuQtfv6CqUfFx1MDAwZv1eYCUtnSjqhjtfv7bNoKWirmdayvjphj3TXHUwMDBio57t+oblt7+6kWqHf8afXHUwMDE1s63+6PptO1xujLGRbWW7kVx1MDAxZoxsKU+1VSdcbnXv/+jvm5u/ks+Ud4GyXCKz0/RU8kBSNXZcdTAwMTBTNlla8TuJs1x1MDAxMENcdTAwMDExxlx1MDAwMr20cMNcdTAwMDNtL1K2rm5on9W4Ji4qoW23o/reoSjzk6FPh+JQXHUwMDBl62OzXHLX8y6ioZe4XHUwMDE1+vptxnVhXHUwMDE0+C117dqRXHUwMDEz254oL3oq8HtNp6PCMPOM3zUtN1x1MDAxYcZlXHUwMDAwvJSOxmBnc1xcMohniFx1MDAwMlx1MDAwM0GJ9GtcbojoS138NELSoIhiXHRYqmbk0b7v6SnQXHUwMDFlfVx1MDAwMcnP2Ke6abWa2rGOPW5DgFx1MDAwNShcdTAwMWa36T+/J2XIwJJQnDbtKLfpRCPvXHJcdTAwMGVcdTAwMDVP2VbJ2ENcdTAwMDRcdTAwMDRHXHUwMDEwkfHUxFx1MDAxNrvHdlx1MDAxMlx1MDAwN/9Ojp5jXHUwMDA23edRKoXxl5S3saOHk0GUXHUwMDBlpNT87pFcdTAwMTCE/UFj96T5sCtxXHUwMDEz/+Wdf3/pK1x1MDAxM3VmXHUwMDEw+P3SS83T839j13pd21x1MDAxY0VcdTAwMTJkXGZxyKCk+u+l3nM7LV3Z6XneuMy3WuPgS0qftlx1MDAxNoh6wkFh1EtcdTAwMGUklZSLuaM+qNTR7vZcdL4/sitn1UrtR3hb21vzqGfAkFx1MDAwMFx0jFx1MDAwMJ6KekJcclx1MDAwMlx1MDAxOYD0vVHfMDV60HTU686ng52xqSiHUlxuXHUwMDAyQGqyVlx1MDAxMuWHu4d30VEzVJWWffvQOFx1MDAwNPBuu5dcdTAwMWblkVx1MDAxYUSpIN/K7zbTemteg5+HnYyfXHUwMDE5siBFsEFcYnKs10syN2peXHUwMDFm5SxqXHUwMDFj03J6gVpcdTAwMDfc0GLcMGbIZeAmXG7MTtg1XHUwMDAzXHUwMDFkqznYoTnYQXhcbjtSYqqdoWL52FlmXHUwMDFjjufb70RcdTAwMTfuY1x1MDAxMksgU1o22643zExZXHUwMDEyoNrTi8hPO2qGSltMVnGeabvruc04gEuWflx1MDAwN1x1MDAxNWRiO3K1rnpp0HZtO81cdTAwMTmWdsDUfVx1MDAwNsfzLPV+4Dbdjuldpv1bnKZQ6jUmaUpALUpcdTAwMDCZX5v1XHUwMDBlr1x1MDAwM+fS/SZJvyo6dXl601x1MDAxMt/Wm6VcYqVcdTAwMDaEnHHNxlNoo9JcdTAwMDBcdTAwMTJyyTL6aFx1MDAxMZZKfvKQhlxySIq0XHUwMDE51lx1MDAwYoEgOEebMY6JXlxiXHRbPvJeY63WXfPQbLdcdTAwMWQk78hjXHUwMDE5n4fSuyrQZsthrXyD68dakMhCXHUwMDE0SUxcdTAwMDSjLKUwZsHo9WFeU9pcIlruXHUwMDE1XHUwMDAySVNcdTAwMWFcdTAwMTV4MpiXTVtcdTAwMDRPY2iatqhcdTAwMDZcdTAwMWRcdTAwMTP4I7CzPqxcdTAwMDXATvJrZFx1MDAwN/TDyWtcdTAwMDZcdTAwMDNMklfazcU5jMvirVx1MDAxNlx1MDAxMZzogYfzi0bYP6uehFF9t7ZrXHUwMDFmPJRZ11xu67X1JjGGpcFFNoVcdTAwMTA/uc1cclx1MDAxZOlUvlx1MDAxM3VfLGVcdTAwMTPbnHeTJSZcdTAwMTHHXHUwMDA0xVxcolXvsS49//6x45fvavVqZ7/Svq5W9uvvYavfq9tZ3Jpv8Fxy3Eo5kzKlXHUwMDBlP4hbXHUwMDE5K1aoVCAuxVx1MDAxYsD9+iivKbUyQnLhXHKJISAj781cdTAwMWMuZy9IuVx1MDAxNrJcdTAwMWN/XHUwMDAwxpdcdTAwMTmB72PVmlxuVbRSPp1BRpN8OnJwcSalQFx1MDAxNGGNx0hcdTAwMTNcZr9cdTAwMDFrXHUwMDAz+O3+7j4kofqhRexpMPjR3f5MXCLFcyXqqeSMTjEpgsYyXHUwMDA0bCGVUsZcZs71ti5tIJOlXHUwMDE3dFwi6/N/nlx1MDAxZSMtcEBqXHUwMDExXFyJno1XXHUwMDFl+Fx1MDAwNuQtXHUwMDFllIyjoqDEXHUwMDE4MoTwXHUwMDFiUlx1MDAxNOft87NK1ynT/burRlx1MDAxN3lcdTAwMTXsW0frre6SRDqmlGTSXHUwMDEwSVTG26qJNOEyj47mTaJTXHUwMDBl9NKPwYrTXHUwMDExe41WebD7/Vie3J62L09cdTAwMDfgxqyU36/EfpduZ1x0vHyD6yfwaHHKX6+4klxuXCLnz528PsprKvCSlH8uwqGuWEJcdTAwMDJyXHUwMDE5XHUwMDEyXHUwMDBmaX2nXaFopYmTVUu8i8hcZlYr8WYw0nS+P3ZwcTYlpJBNIcNcdTAwMTJLXHUwMDFkb/NrvL9r29fSqdVOq4KU/dtcdTAwMDPn4LzZ/FxcuPG5Uv5cdTAwMTTg6VxylTCYpO9l01x1MDAxOVhbLOFPmdB2Kf6Ao7bXOOvCeri+8/f/dlx1MDAwNvXHq+F5w2pcdTAwMGWCo/dT4e/S7SyGzTe4flxmi3nhXHIsxiiIz3Hnh/zro7ymkI9cdTAwMGYnciGvXHRcdTAwMTZ+KLnOdyghpN5eM/BcdTAwMTH4Xlx1MDAxZm79rEOJXHUwMDE5JLXwoURcdTAwMTHcXGIovsNCMJGI0vlcdTAwMDXtzZk68Fx1MDAxYex+31x1MDAxOYKa1+Ku6Fx1MDAwNnTdXHUwMDA1LYlcdTAwMTMpOVlcdTAwMTQujUmVu3TAXHUwMDAxlFx1MDAwM7hpRiWAUckkXm3aXHUwMDA0QcRTm+pVXFxe6ZuR5eTjTeTjzVON6Fx1MDAxNbRlLsRkoZZ5kbybKiNnXHUwMDE2Qlx1MDAxNS1GXHUwMDE1RHqXKKVEb4BVs/5Ar1x1MDAxYSdcdTAwMDdu9fjspkrCI+ZcdTAwMDRrXHUwMDBmK6hcdTAwMTUqZtn7usmjnFx1MDAxOIxcIvyx5+tcdTAwMTLOhSwmXHUwMDEwkECI1e1cdTAwMTOJ5jLGqcSrRJbehil7czGEzWS0xTE26dZcYmtcdTAwMWLPKrRkdru6TVx1MDAxNDs3Qp6eXHUwMDFj135+/XHXpZ+u6u9cdTAwMTXfZNp4xm9cZlx1MDAxNFx1MDAxNU/Nr6eNp/9cdTAwMDDD6SGzIn0= Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch

    We can accomplish this with a CSS class. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.

    Here's the new CSS:

    stopwatch04.tcss
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n\nTimeDisplay {\n    content-align: center middle;\n    text-opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n\n.started {\n    text-style: bold;\n    background: $success;\n    color: $text;\n}\n\n.started TimeDisplay {\n    text-opacity: 100%;\n}\n\n.started #start {\n    display: none\n}\n\n.started #stop {\n    display: block\n}\n\n.started #reset {\n    visibility: hidden\n}\n

    These new rules are prefixed with .started. The . indicates that .started refers to a CSS class called \"started\". The new styles will be applied only to widgets that have this CSS class.

    Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:

    .started #start {\n    display: none\n}\n

    The .started selector matches any widget with a \"started\" CSS class. While #start matches a child widget with an ID of \"start\". So it matches the Start button only for Stopwatches in a started state.

    The rule is \"display: none\" which tells Textual to hide the button.

    "},{"location":"tutorial/#manipulating-classes","title":"Manipulating classes","text":"

    Modifying a widget's CSS classes is a convenient way to update visuals without introducing a lot of messy display related code.

    You can add and remove CSS classes with the add_class() and remove_class() methods. We will use these methods to connect the started state to the Start / Stop buttons.

    The following code will start or stop the stopwatches in response to clicking a button.

    stopwatch04.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        if event.button.id == \"start\":\n            self.add_class(\"started\")\n        elif event.button.id == \"stop\":\n            self.remove_class(\"started\")\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The on_button_pressed method is an event handler. Event handlers are methods called by Textual in response to an event such as a key press, mouse click, etc. Event handlers begin with on_ followed by the name of the event they will handle. Hence on_button_pressed will handle the button pressed event.

    If you run stopwatch04.py now you will be able to toggle between the two states by clicking the first button:

    stopwatch04.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:00.00 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    "},{"location":"tutorial/#reactive-attributes","title":"Reactive attributes","text":"

    A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call refresh() to display new data. However, Textual prefers to do this automatically via reactive attributes.

    You can declare a reactive attribute with reactive. Let's use this feature to create a timer that displays elapsed time and keeps it updated.

    stopwatch05.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.set_interval(1 / 60, self.update_time)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update the time to the current time.\"\"\"\n        self.time = monotonic() - self.start_time\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        if event.button.id == \"start\":\n            self.add_class(\"started\")\n        elif event.button.id == \"stop\":\n            self.remove_class(\"started\")\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    We have added two reactive attributes to the TimeDisplay widget: start_time will contain the time (in seconds) the stopwatch was started, and time will contain the time to be displayed on the Stopwatch.

    Both attributes will be available on self as if you had assigned them in __init__. If you write to either of these attributes the widget will update automatically.

    Info

    The monotonic function in this example is imported from the standard library time module. It is similar to time.time but won't go backwards if the system clock is changed.

    The first argument to reactive may be a default value for the attribute or a callable that returns a default value. We set the default for start_time to the monotonic function which will be called to initialize the attribute with the current time when the TimeDisplay is added to the app. The time attribute has a simple float as the default, so self.time will be initialized to 0.

    The on_mount method is an event handler called when the widget is first added to the application (or mounted in Textual terminology). In this method we call set_interval() to create a timer which calls self.update_time sixty times a second. This update_time method calculates the time elapsed since the widget started and assigns it to self.time \u2014 which brings us to one of Reactive's super-powers.

    If you implement a method that begins with watch_ followed by the name of a reactive attribute, then the method will be called when the attribute is modified. Such methods are known as watch methods.

    Because watch_time watches the time attribute, when we update self.time 60 times a second we also implicitly call watch_time which converts the elapsed time to a string and updates the widget with a call to self.update. Because this happens automatically, we don't need to pass in an initial argument to TimeDisplay.

    The end result is that the Stopwatch widgets show the time elapsed since the widget was created:

    stopwatch05.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.07Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.07Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.08Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate stopwatches independently.

    "},{"location":"tutorial/#wiring-buttons","title":"Wiring buttons","text":"

    We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the TimeDisplay class.

    stopwatch06.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n    def start(self) -> None:\n        \"\"\"Method to start (or resume) time updating.\"\"\"\n        self.start_time = monotonic()\n        self.update_timer.resume()\n\n    def stop(self) -> None:\n        \"\"\"Method to stop the time display updating.\"\"\"\n        self.update_timer.pause()\n        self.total += monotonic() - self.start_time\n        self.time = self.total\n\n    def reset(self) -> None:\n        \"\"\"Method to reset the time display to zero.\"\"\"\n        self.total = 0\n        self.time = 0\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Called to add widgets to the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Here's a summary of the changes made to TimeDisplay.

    • We've added a total reactive attribute to store the total time elapsed between clicking the start and stop buttons.
    • The call to set_interval has grown a pause=True argument which starts the timer in pause mode (when a timer is paused it won't run until resume() is called). This is because we don't want the time to update until the user hits the start button.
    • The update_time method now adds total to the current time to account for the time between any previous clicks of the start and stop buttons.
    • We've stored the result of set_interval which returns a Timer object. We will use this later to resume the timer when we start the Stopwatch.
    • We've added start(), stop(), and reset() methods.

    In addition, the on_button_pressed method on Stopwatch has grown some code to manage the time display when the user clicks a button. Let's look at that in detail:

        def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n

    This code supplies missing features and makes our app useful. We've made the following changes.

    • The first line retrieves id attribute of the button that was pressed. We can use this to decide what to do in response.
    • The second line calls query_one to get a reference to the TimeDisplay widget.
    • We call the method on TimeDisplay that matches the pressed button.
    • We add the \"started\" class when the Stopwatch is started (self.add_class(\"started\")), and remove it (self.remove_class(\"started\")) when it is stopped. This will update the Stopwatch visuals via CSS.

    If you run stopwatch06.py you will be able to use the stopwatches independently.

    stopwatch06.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:10.09 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:06.03 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.

    "},{"location":"tutorial/#dynamic-widgets","title":"Dynamic widgets","text":"

    The Stopwatch app creates widgets when it starts via the compose method. We will also need to create new widgets while the app is running, and remove widgets we no longer need. We can do this by calling mount() to add a widget, and remove() to remove a widget.

    Let's use these methods to implement adding and removing stopwatches to our app.

    stopwatch.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n    def start(self) -> None:\n        \"\"\"Method to start (or resume) time updating.\"\"\"\n        self.start_time = monotonic()\n        self.update_timer.resume()\n\n    def stop(self):\n        \"\"\"Method to stop the time display updating.\"\"\"\n        self.update_timer.pause()\n        self.total += monotonic() - self.start_time\n        self.time = self.total\n\n    def reset(self):\n        \"\"\"Method to reset the time display to zero.\"\"\"\n        self.total = 0\n        self.time = 0\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch.tcss\"\n\n    BINDINGS = [\n        (\"d\", \"toggle_dark\", \"Toggle dark mode\"),\n        (\"a\", \"add_stopwatch\", \"Add\"),\n        (\"r\", \"remove_stopwatch\", \"Remove\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Called to add widgets to the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch(), id=\"timers\")\n\n    def action_add_stopwatch(self) -> None:\n        \"\"\"An action to add a timer.\"\"\"\n        new_stopwatch = Stopwatch()\n        self.query_one(\"#timers\").mount(new_stopwatch)\n        new_stopwatch.scroll_visible()\n\n    def action_remove_stopwatch(self) -> None:\n        \"\"\"Called to remove a timer.\"\"\"\n        timers = self.query(\"Stopwatch\")\n        if timers:\n            timers.last().remove()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Here's a summary of the changes:

    • The ScrollableContainer object in StopWatchApp grew a \"timers\" ID.
    • Added action_add_stopwatch to add a new stopwatch.
    • Added action_remove_stopwatch to remove a stopwatch.
    • Added keybindings for the actions.

    The action_add_stopwatch method creates and mounts a new stopwatch. Note the call to query_one() with a CSS selector of \"#timers\" which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in action_add_stopwatch calls scroll_visible() which will scroll the container to make the new Stopwatch visible (if required).

    The action_remove_stopwatch function calls query() with a CSS selector of \"Stopwatch\" which gets all the Stopwatch widgets. If there are stopwatches then the action calls last() to get the last stopwatch, and remove() to remove it.

    If you run stopwatch.py now you can add a new stopwatch with the A key and remove a stopwatch with R.

    stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:06.09 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    "},{"location":"tutorial/#what-next","title":"What next?","text":"

    Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.

    Read the guide for the full details on how to build sophisticated TUI applications with Textual.

    "},{"location":"widget_gallery/","title":"Widgets","text":"

    Welcome to the Textual widget gallery.

    We have many more widgets planned, or you can build your own.

    Info

    Textual is a TUI framework. Everything below runs in the terminal.

    "},{"location":"widget_gallery/#button","title":"Button","text":"

    A simple button with a variety of semantic styles.

    Button reference

    ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DefaultDefault \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Primary!Primary! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Success!Success! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Warning!Warning! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Error!Error! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"widget_gallery/#checkbox","title":"Checkbox","text":"

    A classic checkbox control.

    Checkbox reference

    CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

    "},{"location":"widget_gallery/#collapsible","title":"Collapsible","text":"

    Content that may be toggled on and off by clicking a title.

    Collapsible reference

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    "},{"location":"widget_gallery/#contentswitcher","title":"ContentSwitcher","text":"

    A widget for containing and switching display between multiple child widgets.

    ContentSwitcher reference

    "},{"location":"widget_gallery/#datatable","title":"DataTable","text":"

    A powerful data table, with configurable cursors.

    DataTable reference

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    "},{"location":"widget_gallery/#digits","title":"Digits","text":"

    Display numbers in tall characters.

    Digits reference

    DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

    "},{"location":"widget_gallery/#directorytree","title":"DirectoryTree","text":"

    A tree view of files and folders.

    DirectoryTree reference

    DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.faq \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.git \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.github \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.ipynb_checkpoints \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.mypy_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.pytest_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.screenshot_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.vscode \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist\u2581\u2581 \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0.coverage

    "},{"location":"widget_gallery/#footer","title":"Footer","text":"

    A footer to display and interact with key bindings.

    Footer reference

    FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

    "},{"location":"widget_gallery/#header","title":"Header","text":"

    A header to display the app's title and subtitle.

    Header reference

    HeaderApp \u2b58HeaderApp

    "},{"location":"widget_gallery/#input","title":"Input","text":"

    A control to enter text.

    Input reference

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#label","title":"Label","text":"

    A simple text label.

    Label reference

    "},{"location":"widget_gallery/#listview","title":"ListView","text":"

    Display a list of items (items may be other widgets).

    ListView reference

    ListViewExample One Two Three

    "},{"location":"widget_gallery/#loadingindicator","title":"LoadingIndicator","text":"

    Display an animation while data is loading.

    LoadingIndicator reference

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    "},{"location":"widget_gallery/#log","title":"Log","text":"

    Display and update lines of text (such as from a file).

    Log reference

    LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    "},{"location":"widget_gallery/#markdownviewer","title":"MarkdownViewer","text":"

    Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).

    MarkdownViewer reference

    MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

    "},{"location":"widget_gallery/#markdown","title":"Markdown","text":"

    Display a markdown document.

    Markdown reference

    MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

    "},{"location":"widget_gallery/#optionlist","title":"OptionList","text":"

    Display a vertical list of options (options may be Rich renderables).

    OptionList reference

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#placeholder","title":"Placeholder","text":"

    Display placeholder content while you are designing a UI.

    Placeholder reference

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

    "},{"location":"widget_gallery/#pretty","title":"Pretty","text":"

    Display a pretty-formatted Rich renderable.

    Pretty reference

    PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

    "},{"location":"widget_gallery/#progressbar","title":"ProgressBar","text":"

    A configurable progress bar with ETA and percentage complete.

    ProgressBar reference

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250150% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$50\u00a0received!

    "},{"location":"widget_gallery/#radiobutton","title":"RadioButton","text":"

    A simple radio button.

    RadioButton reference

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#radioset","title":"RadioSet","text":"

    A collection of radio buttons, that enforces uniqueness.

    RadioSet reference

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#richlog","title":"RichLog","text":"

    Display and update text in a scrolling panel.

    RichLog reference

    RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

    "},{"location":"widget_gallery/#rule","title":"Rule","text":"

    A rule widget to separate content, similar to a <hr> HTML tag.

    Rule reference

    HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

    "},{"location":"widget_gallery/#select","title":"Select","text":"

    Select from a number of possible options.

    Select reference

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#selectionlist","title":"SelectionList","text":"

    Select multiple values from a list of options.

    SelectionList reference

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"widget_gallery/#sparkline","title":"Sparkline","text":"

    Display numerical data.

    Sparkline reference

    SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"widget_gallery/#static","title":"Static","text":"

    Displays simple static content. Typically used as a base class.

    Static reference

    "},{"location":"widget_gallery/#switch","title":"Switch","text":"

    An on / off control, inspired by toggle buttons.

    Switch reference

    SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#tabs","title":"Tabs","text":"

    A row of tabs you can select with the mouse or navigate with keys.

    Tabs reference

    TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    "},{"location":"widget_gallery/#tabbedcontent","title":"TabbedContent","text":"

    A Combination of Tabs and ContentSwitcher to navigate static content.

    TabbedContent reference

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    "},{"location":"widget_gallery/#textarea","title":"TextArea","text":"

    A multi-line text area which supports syntax highlighting various languages.

    TextArea reference

    TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

    "},{"location":"widget_gallery/#tree","title":"Tree","text":"

    A tree control with expandable nodes.

    Tree reference

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    "},{"location":"api/","title":"API","text":"

    This is a API-level reference to the Textual API. Click the links to your left (or in the burger menu) to open a reference for each module.

    If you are new to Textual, you may want to read the tutorial or guide first.

    "},{"location":"api/app/","title":"App","text":"

    Here you will find the App class, which is the base class for Textual apps.

    See app basics for how to build Textual apps.

    "},{"location":"api/app/#textual.app.AutopilotCallbackType","title":"AutopilotCallbackType module-attribute","text":"
    AutopilotCallbackType: TypeAlias = (\n    \"Callable[[Pilot[object]], Coroutine[Any, Any, None]]\"\n)\n

    Signature for valid callbacks that can be used to control apps.

    "},{"location":"api/app/#textual.app.CSSPathType","title":"CSSPathType module-attribute","text":"
    CSSPathType = Union[\n    str, PurePath, List[Union[str, PurePath]]\n]\n

    Valid ways of specifying paths to CSS files.

    "},{"location":"api/app/#textual.app.ActionError","title":"ActionError class","text":"

    Bases: Exception

    Base class for exceptions relating to actions.

    "},{"location":"api/app/#textual.app.ActiveModeError","title":"ActiveModeError class","text":"

    Bases: ModeError

    Raised when attempting to remove the currently active mode.

    "},{"location":"api/app/#textual.app.App","title":"App class","text":"
    def __init__(\n    self, driver_class=None, css_path=None, watch_css=False\n):\n

    Bases: Generic[ReturnType], DOMNode

    The base class for Textual Applications.

    Parameters Parameter Default Description driver_class Type[Driver] | None None

    Driver class or None to auto-detect. This will be used by some Textual tools.

    css_path CSSPathType | None None

    Path to CSS or None to use the CSS_PATH class variable. To load multiple CSS files, pass a list of strings or paths which will be loaded in order.

    watch_css bool False

    Reload CSS if the files changed. This is set automatically if you are using textual run with the dev switch.

    Raises Type Description CssPathError

    When the supplied CSS path(s) are an unexpected type.

    "},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS: str | None = '*'\n

    A selector to determine what to focus automatically when a screen is activated.

    The widget focused is the first that matches the given CSS selector. Setting to None or \"\" disables auto focus.

    "},{"location":"api/app/#textual.app.App.COMMANDS","title":"COMMANDS class-attribute","text":"
    COMMANDS: set[\n    type[Provider] | Callable[[], type[Provider]]\n] = {get_system_commands}\n

    Command providers used by the command palette.

    Should be a set of command.Provider classes.

    "},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
    CSS: str = ''\n

    Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, and therefore takes priority in the event of a specificity clash.

    "},{"location":"api/app/#textual.app.App.CSS_PATH","title":"CSS_PATH class-attribute","text":"
    CSS_PATH: CSSPathType | None = None\n

    File paths to load CSS from.

    "},{"location":"api/app/#textual.app.App.ENABLE_COMMAND_PALETTE","title":"ENABLE_COMMAND_PALETTE class-attribute","text":"
    ENABLE_COMMAND_PALETTE: bool = True\n

    Should the command palette be enabled for the application?

    "},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
    MODES: dict[str, str | Screen | Callable[[], Screen]] = {}\n

    Modes associated with the app and their base screens.

    The base screen is the screen at the bottom of the mode stack. You can think of it as the default screen for that stack. The base screens can be names of screens listed in SCREENS, Screen instances, or callables that return screens.

    Example
    class HelpScreen(Screen[None]):\n    ...\n\nclass MainAppScreen(Screen[None]):\n    ...\n\nclass MyApp(App[None]):\n    MODES = {\n        \"default\": \"main\",\n        \"help\": HelpScreen,\n    }\n\n    SCREENS = {\n        \"main\": MainAppScreen,\n    }\n\n    ...\n
    "},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
    SCREENS: dict[\n    str, Screen[Any] | Callable[[], Screen[Any]]\n] = {}\n

    Screens associated with the app for the lifetime of the app.

    "},{"location":"api/app/#textual.app.App.SUB_TITLE","title":"SUB_TITLE instance-attribute class-attribute","text":"
    SUB_TITLE: str | None = None\n

    A class variable to set the default sub-title for the application.

    To update the sub-title while the app is running, you can set the sub_title attribute. See also the Screen.SUB_TITLE attribute.

    "},{"location":"api/app/#textual.app.App.TITLE","title":"TITLE instance-attribute class-attribute","text":"
    TITLE: str | None = None\n

    A class variable to set the default title for the application.

    To update the title while the app is running, you can set the title attribute. See also the Screen.TITLE attribute.

    "},{"location":"api/app/#textual.app.App.app_focus","title":"app_focus instance-attribute class-attribute","text":"
    app_focus = Reactive(True, compute=False)\n

    Indicates if the app has focus.

    When run in the terminal, the app always has focus. When run in the web, the app will get focus when the terminal widget has focus.

    "},{"location":"api/app/#textual.app.App.children","title":"children property","text":"
    children: Sequence['Widget']\n

    A view onto the app's immediate children.

    This attribute exists on all widgets. In the case of the App, it will only ever contain a single child, which will be the currently active screen.

    Returns Type Description Sequence['Widget']

    A sequence of widgets.

    "},{"location":"api/app/#textual.app.App.current_mode","title":"current_mode property","text":"
    current_mode: str\n

    The name of the currently active mode.

    "},{"location":"api/app/#textual.app.App.cursor_position","title":"cursor_position instance-attribute","text":"
    cursor_position = Offset(0, 0)\n

    The position of the terminal cursor in screen-space.

    This can be set by widgets and is useful for controlling the positioning of OS IME and emoji popup menus.

    "},{"location":"api/app/#textual.app.App.dark","title":"dark instance-attribute class-attribute","text":"
    dark: Reactive[bool] = Reactive(True, compute=False)\n

    Use a dark theme if True, otherwise use a light theme.

    Modify this attribute to switch between light and dark themes.

    Example
    self.app.dark = not self.app.dark  # Toggle dark mode\n
    "},{"location":"api/app/#textual.app.App.debug","title":"debug property","text":"
    debug: bool\n

    Is debug mode enabled?

    "},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
    focused: Widget | None\n

    The widget that is focused on the currently active screen, or None.

    Focused widgets receive keyboard input.

    Returns Type Description Widget | None

    The currently focused widget, or None if nothing is focused.

    "},{"location":"api/app/#textual.app.App.is_headless","title":"is_headless property","text":"
    is_headless: bool\n

    Is the driver running in 'headless' mode?

    Headless mode is used when running tests with run_test.

    "},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
    log: Logger\n

    The textual logger.

    Example
    self.log(\"Hello, World!\")\nself.log(self.tree)\n
    Returns Type Description Logger

    A Textual logger.

    "},{"location":"api/app/#textual.app.App.namespace_bindings","title":"namespace_bindings property","text":"
    namespace_bindings: dict[str, tuple[DOMNode, Binding]]\n

    Get currently active bindings.

    If no widget is focused, then app-level bindings are returned. If a widget is focused, then any bindings present in the active screen and app are merged and returned.

    This property may be used to inspect current bindings.

    Returns Type Description dict[str, tuple[DOMNode, Binding]]

    A mapping of keys onto pairs of nodes and bindings.

    "},{"location":"api/app/#textual.app.App.return_code","title":"return_code property","text":"
    return_code: int | None\n

    The return code with which the app exited.

    Non-zero codes indicate errors. A value of 1 means the app exited with a fatal error. If the app wasn't exited yet, this will be None.

    Example

    The return code can be used to exit the process via sys.exit.

    my_app.run()\nsys.exit(my_app.return_code)\n

    "},{"location":"api/app/#textual.app.App.return_value","title":"return_value property","text":"
    return_value: ReturnType | None\n

    The return value of the app, or None if it has not yet been set.

    The return value is set when calling exit.

    "},{"location":"api/app/#textual.app.App.screen","title":"screen property","text":"
    screen: Screen[object]\n

    The current active screen.

    Returns Type Description Screen[object]

    The currently active (visible) screen.

    Raises Type Description ScreenStackError

    If there are no screens on the stack.

    "},{"location":"api/app/#textual.app.App.screen_stack","title":"screen_stack property","text":"
    screen_stack: Sequence[Screen]\n

    A snapshot of the current screen stack.

    Returns Type Description Sequence[Screen]

    A snapshot of the current state of the screen stack.

    "},{"location":"api/app/#textual.app.App.scroll_sensitivity_x","title":"scroll_sensitivity_x instance-attribute","text":"
    scroll_sensitivity_x: float = 4.0\n

    Number of columns to scroll in the X direction with wheel or trackpad.

    "},{"location":"api/app/#textual.app.App.scroll_sensitivity_y","title":"scroll_sensitivity_y instance-attribute","text":"
    scroll_sensitivity_y: float = 2.0\n

    Number of lines to scroll in the Y direction with wheel or trackpad.

    "},{"location":"api/app/#textual.app.App.size","title":"size property","text":"
    size: Size\n

    The size of the terminal.

    Returns Type Description Size

    Size of the terminal.

    "},{"location":"api/app/#textual.app.App.sub_title","title":"sub_title instance-attribute class-attribute","text":"
    sub_title: Reactive[str] = (\n    self.SUB_TITLE if self.SUB_TITLE is not None else \"\"\n)\n

    The sub-title for the application.

    The initial value for sub_title will be set to the SUB_TITLE class variable if it exists, or an empty string if it doesn't.

    Sub-titles are typically used to show the high-level state of the app, such as the current mode, or path to the file being worked on.

    Assign a new value to this attribute to change the sub-title. The new value is always converted to string.

    "},{"location":"api/app/#textual.app.App.title","title":"title instance-attribute class-attribute","text":"
    title: Reactive[str] = (\n    self.TITLE\n    if self.TITLE is not None\n    else f\"{self.__class__.__name__}\"\n)\n

    The title for the application.

    The initial value for title will be set to the TITLE class variable if it exists, or the name of the app if it doesn't.

    Assign a new value to this attribute to change the title. The new value is always converted to string.

    "},{"location":"api/app/#textual.app.App.use_command_palette","title":"use_command_palette instance-attribute","text":"
    use_command_palette: bool = self.ENABLE_COMMAND_PALETTE\n

    A flag to say if the application should use the command palette.

    If set to False any call to action_command_palette will be ignored.

    "},{"location":"api/app/#textual.app.App.workers","title":"workers property","text":"
    workers: WorkerManager\n

    The worker manager.

    Returns Type Description WorkerManager

    An object to manage workers.

    "},{"location":"api/app/#textual.app.App.action_add_class","title":"action_add_class async","text":"
    def action_add_class(self, selector, class_name):\n

    An action to add a CSS class to the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to add the class to.

    class_name str required

    The class to add to the selected widget.

    "},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
    def action_back(self):\n

    An action to go back to the previous screen (pop the current screen).

    Note

    If there is no screen to go back to, this is a non-operation (in other words it's safe to call even if there are no other screens on the stack.)

    "},{"location":"api/app/#textual.app.App.action_bell","title":"action_bell async","text":"
    def action_bell(self):\n

    An action to play the terminal 'bell'.

    "},{"location":"api/app/#textual.app.App.action_check_bindings","title":"action_check_bindings async","text":"
    def action_check_bindings(self, key):\n

    An action to handle a key press using the binding system.

    Parameters Parameter Default Description key str required

    The key to process.

    "},{"location":"api/app/#textual.app.App.action_command_palette","title":"action_command_palette method","text":"
    def action_command_palette(self):\n

    Show the Textual command palette.

    "},{"location":"api/app/#textual.app.App.action_focus","title":"action_focus async","text":"
    def action_focus(self, widget_id):\n

    An action to focus the given widget.

    Parameters Parameter Default Description widget_id str required

    ID of widget to focus.

    "},{"location":"api/app/#textual.app.App.action_focus_next","title":"action_focus_next method","text":"
    def action_focus_next(self):\n

    An action to focus the next widget.

    "},{"location":"api/app/#textual.app.App.action_focus_previous","title":"action_focus_previous method","text":"
    def action_focus_previous(self):\n

    An action to focus the previous widget.

    "},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
    def action_pop_screen(self):\n

    An action to remove the topmost screen and makes the new topmost screen active.

    "},{"location":"api/app/#textual.app.App.action_push_screen","title":"action_push_screen async","text":"
    def action_push_screen(self, screen):\n

    An action to push a new screen on to the stack and make it active.

    Parameters Parameter Default Description screen str required

    Name of the screen.

    "},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
    def action_quit(self):\n

    An action to quit the app as soon as possible.

    "},{"location":"api/app/#textual.app.App.action_remove_class","title":"action_remove_class async","text":"
    def action_remove_class(self, selector, class_name):\n

    An action to remove a CSS class from the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to remove the class from.

    class_name str required

    The class to remove from the selected widget.

    "},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot method","text":"
    def action_screenshot(self, filename=None, path='./'):\n

    This action will save an SVG file containing the current contents of the screen.

    Parameters Parameter Default Description filename str | None None

    Filename of screenshot, or None to auto-generate.

    path str './'

    Path to directory. Defaults to current working directory.

    "},{"location":"api/app/#textual.app.App.action_switch_mode","title":"action_switch_mode async","text":"
    def action_switch_mode(self, mode):\n

    An action that switches to the given mode..

    "},{"location":"api/app/#textual.app.App.action_switch_screen","title":"action_switch_screen async","text":"
    def action_switch_screen(self, screen):\n

    An action to switch screens.

    Parameters Parameter Default Description screen str required

    Name of the screen.

    "},{"location":"api/app/#textual.app.App.action_toggle_class","title":"action_toggle_class async","text":"
    def action_toggle_class(self, selector, class_name):\n

    An action to toggle a CSS class on the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to toggle the class on.

    class_name str required

    The class to toggle on the selected widget.

    "},{"location":"api/app/#textual.app.App.action_toggle_dark","title":"action_toggle_dark method","text":"
    def action_toggle_dark(self):\n

    An action to toggle dark mode.

    "},{"location":"api/app/#textual.app.App.add_mode","title":"add_mode method","text":"
    def add_mode(self, mode, base_screen):\n

    Adds a mode and its corresponding base screen to the app.

    Parameters Parameter Default Description mode str required

    The new mode.

    base_screen str | Screen | Callable[[], Screen] required

    The base screen associated with the given mode.

    Raises Type Description InvalidModeError

    If the name of the mode is not valid/duplicated.

    "},{"location":"api/app/#textual.app.App.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    See the guide for how to use the animation system.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update method","text":"
    def batch_update(self):\n

    A context manager to suspend all repaints until the end of the batch.

    "},{"location":"api/app/#textual.app.App.begin_capture_print","title":"begin_capture_print method","text":"
    def begin_capture_print(self, target, stdout=True, stderr=True):\n

    Capture content that is printed (or written to stdout / stderr).

    If printing is captured, the target will be sent an events.Print message.

    Parameters Parameter Default Description target MessageTarget required

    The widget where print content will be sent.

    stdout bool True

    Capture stdout.

    stderr bool True

    Capture stderr.

    "},{"location":"api/app/#textual.app.App.bell","title":"bell method","text":"
    def bell(self):\n

    Play the console 'bell'.

    For terminals that support a bell, this typically makes a notification or error sound. Some terminals may make no sound or display a visual bell indicator, depending on configuration.

    "},{"location":"api/app/#textual.app.App.bind","title":"bind method","text":"
    def bind(\n    self,\n    keys,\n    action,\n    *,\n    description=\"\",\n    show=True,\n    key_display=None\n):\n

    Bind a key to an action.

    Parameters Parameter Default Description keys str required

    A comma separated list of keys, i.e.

    action str required

    Action to bind to.

    description str ''

    Short description of action.

    show bool True

    Show key in UI.

    key_display str | None None

    Replacement text for key, or None to use default.

    "},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread method","text":"
    def call_from_thread(self, callback, *args, **kwargs):\n

    Run a callable from another thread, and return the result.

    Like asyncio apps in general, Textual apps are not thread-safe. If you call methods or set attributes on Textual objects from a thread, you may get unpredictable results.

    This method will ensure that your code runs within the correct context.

    Tip

    Consider using post_message which is also thread-safe.

    Parameters Parameter Default Description callback Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]] required

    A callable to run.

    *args Any ()

    Arguments to the callback.

    **kwargs Any {}

    Keyword arguments for the callback.

    Raises Type Description RuntimeError

    If the app isn't running or if this method is called from the same thread where the app is running.

    Returns Type Description CallThreadReturnType

    The result of the callback.

    "},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse method","text":"
    def capture_mouse(self, widget):\n

    Send all mouse events to the given widget or disable mouse capture.

    Parameters Parameter Default Description widget Widget | None required

    If a widget, capture mouse event, or None to end mouse capture.

    "},{"location":"api/app/#textual.app.App.check_bindings","title":"check_bindings async","text":"
    def check_bindings(self, key, priority=False):\n

    Handle a key press.

    This method is used internally by the bindings system, but may be called directly if you wish to simulate a key being pressed.

    Parameters Parameter Default Description key str required

    A key.

    priority bool False

    If True check from App down, otherwise from focused up.

    Returns Type Description bool

    True if the key was handled by a binding, otherwise False

    "},{"location":"api/app/#textual.app.App.clear_notifications","title":"clear_notifications method","text":"
    def clear_notifications(self):\n

    Clear all the current notifications.

    "},{"location":"api/app/#textual.app.App.compose","title":"compose method","text":"
    def compose(self):\n

    Yield child widgets for a container.

    This method should be implemented in a subclass.

    "},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print method","text":"
    def end_capture_print(self, target):\n

    End capturing of prints.

    Parameters Parameter Default Description target MessageTarget required

    The widget that was capturing prints.

    "},{"location":"api/app/#textual.app.App.exit","title":"exit method","text":"
    def exit(self, result=None, return_code=0, message=None):\n

    Exit the app, and return the supplied result.

    Parameters Parameter Default Description result ReturnType | None None

    Return value.

    return_code int 0

    The return code. Use non-zero values for error codes.

    message RenderableType | None None

    Optional message to display on exit.

    "},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot method","text":"
    def export_screenshot(self, *, title=None):\n

    Export an SVG screenshot of the current screen.

    See also save_screenshot which writes the screenshot to a file.

    Parameters Parameter Default Description title str | None None

    The title of the exported screenshot or None to use app title.

    "},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id method","text":"
    def get_child_by_id(self, id, expect_type=None):\n

    Get the first child (immediate descendent) of this DOMNode with the given ID.

    Parameters Parameter Default Description id str required

    The ID of the node to search for.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or use None to apply no type restriction.

    Returns Type Description ExpectType | Widget

    The first child of this node with the specified ID.

    Raises Type Description NoMatches

    If no children could be found for this ID.

    WrongType

    If the wrong type was found.

    "},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type method","text":"
    def get_child_by_type(self, expect_type):\n

    Get a child of a give type.

    Parameters Parameter Default Description expect_type type[ExpectType] required

    The type of the expected child.

    Raises Type Description NoMatches

    If no valid child is found.

    Returns Type Description ExpectType

    A widget.

    "},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables method","text":"
    def get_css_variables(self):\n

    Get a mapping of variables used to pre-populate CSS.

    May be implemented in a subclass to add new CSS variables.

    Returns Type Description dict[str, str]

    A mapping of variable name to value.

    "},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class method","text":"
    def get_driver_class(self):\n

    Get a driver class for this platform.

    This method is called by the constructor, and unlikely to be required when building a Textual app.

    Returns Type Description Type[Driver]

    A Driver class which manages input and display.

    "},{"location":"api/app/#textual.app.App.get_key_display","title":"get_key_display method","text":"
    def get_key_display(self, key):\n

    For a given key, return how it should be displayed in an app (e.g. in the Footer widget). By key, we refer to the string used in the \"key\" argument for a Binding instance. By overriding this method, you can ensure that keys are displayed consistently throughout your app, without needing to add a key_display to every binding.

    Parameters Parameter Default Description key str required

    The binding key string.

    Returns Type Description str

    The display string for the input key.

    "},{"location":"api/app/#textual.app.App.get_loading_widget","title":"get_loading_widget method","text":"
    def get_loading_widget(self):\n

    Get a widget to be used as a loading indicator.

    Extend this method if you want to display the loading state a little differently.

    Returns Type Description Widget

    A widget to display a loading state.

    "},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen method","text":"
    def get_screen(self, screen):\n

    Get an installed screen.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    Raises Type Description KeyError

    If the named screen doesn't exist.

    Returns Type Description Screen

    A screen instance.

    "},{"location":"api/app/#textual.app.App.get_widget_at","title":"get_widget_at method","text":"
    def get_widget_at(self, x, y):\n

    Get the widget under the given coordinates.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description tuple[Widget, Region]

    The widget and the widget's screen region.

    "},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id method","text":"
    def get_widget_by_id(self, id, expect_type=None):\n

    Get the first descendant widget with the given ID.

    Performs a breadth-first search rooted at the current screen. It will not return the Screen if that matches the ID. To get the screen, use self.screen.

    Parameters Parameter Default Description id str required

    The ID to search for in the subtree

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type. Defaults to None.

    Returns Type Description ExpectType | Widget

    The first descendant encountered with this ID.

    Raises Type Description NoMatches

    if no children could be found for this ID

    WrongType

    if the wrong type was found.

    "},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen method","text":"
    def install_screen(self, screen, name):\n

    Install a screen.

    Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. Note that you don't need to install a screen to use it. See push_screen or switch_screen to make a new screen current.

    Parameters Parameter Default Description screen Screen required

    Screen to install.

    name str required

    Unique name to identify the screen.

    Raises Type Description ScreenError

    If the screen can't be installed.

    Returns Type Description None

    An awaitable that awaits the mounting of the screen and its children.

    "},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted method","text":"
    def is_mounted(self, widget):\n

    Check if a widget is mounted.

    Parameters Parameter Default Description widget Widget required

    A widget.

    Returns Type Description bool

    True of the widget is mounted.

    "},{"location":"api/app/#textual.app.App.is_screen_installed","title":"is_screen_installed method","text":"
    def is_screen_installed(self, screen):\n

    Check if a given screen has been installed.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    Returns Type Description bool

    True if the screen is currently installed,

    "},{"location":"api/app/#textual.app.App.mount","title":"mount method","text":"
    def mount(self, *widgets, before=None, after=None):\n

    Mount the given widgets relative to the app's screen.

    Parameters Parameter Default Description *widgets Widget ()

    The widget(s) to mount.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all method","text":"
    def mount_all(self, widgets, *, before=None, after=None):\n

    Mount widgets from an iterable.

    Parameters Parameter Default Description widgets Iterable[Widget] required

    An iterable of widgets.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/app/#textual.app.App.notify","title":"notify method","text":"
    def notify(\n    self,\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=Notification.timeout\n):\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters Parameter Default Description message str required

    The message for the notification.

    title str ''

    The title for the notification.

    severity SeverityLevel 'information'

    The severity of the notification.

    timeout float Notification.timeout

    The timeout for the notification.

    The notify method is used to create an application-wide notification, shown in a Toast, normally originating in the bottom right corner of the display.

    Notifications can have the following severity levels:

    • information
    • warning
    • error

    The default is information.

    Example
    # Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n    \"Now witness the firepower of this fully \"\n    \"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\n    title=\"Possible trap detected\",\n    severity=\"warning\",\n)\n\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n
    "},{"location":"api/app/#textual.app.App.panic","title":"panic method","text":"
    def panic(self, *renderables):\n

    Exits the app and display error message(s).

    Used in response to unexpected errors. For a more graceful exit, see the exit method.

    Parameters Parameter Default Description *renderables RenderableType ()

    Text or Rich renderable(s) to display on exit.

    "},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen method","text":"
    def pop_screen(self):\n

    Pop the current screen from the stack, and switch to the previous screen.

    Returns Type Description Screen[object]

    The screen that was replaced.

    "},{"location":"api/app/#textual.app.App.post_display_hook","title":"post_display_hook method","text":"
    def post_display_hook(self):\n

    Called immediately after a display is done. Used in tests.

    "},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen method","text":"
    def push_screen(\n    self, screen, callback=None, wait_for_dismiss=False\n):\n

    Push a new screen on the screen stack, making it the current screen.

    Parameters Parameter Default Description screen Screen[ScreenResultType] | str required

    A Screen instance or the name of an installed screen.

    callback ScreenResultCallbackType[ScreenResultType] | None None

    An optional callback function that will be called if the screen is dismissed with a result.

    wait_for_dismiss bool False

    If True, awaiting this method will return the dismiss value from the screen. When set to False, awaiting this method will wait for the screen to be mounted. Note that wait_for_dismiss should only be set to True when running in a worker.

    Raises Type Description NoActiveWorker

    If using wait_for_dismiss outside of a worker.

    Returns Type Description AwaitMount | asyncio.Future[ScreenResultType]

    An optional awaitable that awaits the mounting of the screen and its children, or an asyncio Future to await the result of the screen.

    "},{"location":"api/app/#textual.app.App.push_screen_wait","title":"push_screen_wait async","text":"
    def push_screen_wait(self, screen):\n

    Push a screen and wait for the result (received from Screen.dismiss).

    Note that this method may only be called when running in a worker.

    Parameters Parameter Default Description screen Screen[ScreenResultType] | str required

    A screen or the name of an installed screen.

    Returns Type Description ScreenResultType | Any

    The screen's result.

    "},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css method","text":"
    def refresh_css(self, animate=True):\n

    Refresh CSS.

    Parameters Parameter Default Description animate bool True

    Also execute CSS animations.

    "},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode method","text":"
    def remove_mode(self, mode):\n

    Removes a mode from the app.

    Screens that are running in the stack of that mode are scheduled for pruning.

    Parameters Parameter Default Description mode str required

    The mode to remove. It can't be the active mode.

    Raises Type Description ActiveModeError

    If trying to remove the active mode.

    UnknownModeError

    If trying to remove an unknown mode.

    "},{"location":"api/app/#textual.app.App.run","title":"run method","text":"
    def run(self, *, headless=False, size=None, auto_pilot=None):\n

    Run the app.

    Parameters Parameter Default Description headless bool False

    Run in headless mode (no output).

    size tuple[int, int] | None None

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    auto_pilot AutopilotCallbackType | None None

    An auto pilot coroutine.

    Returns Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
    def run_action(self, action, default_namespace=None):\n

    Perform an action.

    Actions are typically associated with key bindings, where you wouldn't need to call this method manually.

    Parameters Parameter Default Description action str | ActionParseResult required

    Action encoded in a string.

    default_namespace object | None None

    Namespace to use if not provided in the action, or None to use app.

    Returns Type Description bool

    True if the event has been handled.

    "},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
    def run_async(\n    self, *, headless=False, size=None, auto_pilot=None\n):\n

    Run the app asynchronously.

    Parameters Parameter Default Description headless bool False

    Run in headless mode (no output).

    size tuple[int, int] | None None

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    auto_pilot AutopilotCallbackType | None None

    An auto pilot coroutine.

    Returns Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
    def run_test(\n    self,\n    *,\n    headless=True,\n    size=(80, 24),\n    tooltips=False,\n    notifications=False,\n    message_hook=None\n):\n

    An asynchronous context manager for testing apps.

    Tip

    See the guide for testing Textual apps.

    Use this to run your app in \"headless\" mode (no output) and drive the app via a Pilot object.

    Example
    async with app.run_test() as pilot:\n    await pilot.click(\"#Button.ok\")\n    assert ...\n
    Parameters Parameter Default Description headless bool True

    Run in headless mode (no output or input).

    size tuple[int, int] | None (80, 24)

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    tooltips bool False

    Enable tooltips when testing.

    notifications bool False

    Enable notifications when testing.

    message_hook Callable[[Message], None] | None None

    An optional callback that will be called each time any message arrives at any message pump in the app.

    "},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot method","text":"
    def save_screenshot(\n    self, filename=None, path=\"./\", time_format=None\n):\n

    Save an SVG screenshot of the current screen.

    Parameters Parameter Default Description filename str | None None

    Filename of SVG screenshot, or None to auto-generate a filename with the date and time.

    path str './'

    Path to directory for output. Defaults to current working directory.

    time_format str | None None

    Date and time format to use if filename is None. Defaults to a format like ISO 8601 with some reserved characters replaced with underscores.

    Returns Type Description str

    Filename of screenshot.

    "},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus method","text":"
    def set_focus(self, widget, scroll_visible=True):\n

    Focus (or unfocus) a widget. A focused widget will receive key events first.

    Parameters Parameter Default Description widget Widget | None required

    Widget to focus.

    scroll_visible bool True

    Scroll widget in to view.

    "},{"location":"api/app/#textual.app.App.stop_animation","title":"stop_animation async","text":"
    def stop_animation(self, attribute, complete=True):\n

    Stop an animation on an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute whose animation should be stopped.

    complete bool True

    Should the animation be set to its final value?

    Note

    If there is no animation scheduled or running, this is a no-op.

    "},{"location":"api/app/#textual.app.App.switch_mode","title":"switch_mode method","text":"
    def switch_mode(self, mode):\n

    Switch to a given mode.

    Parameters Parameter Default Description mode str required

    The mode to switch to.

    Returns Type Description AwaitMount

    An optionally awaitable object which waits for the screen associated with the mode to be mounted.

    Raises Type Description UnknownModeError

    If trying to switch to an unknown mode.

    "},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen method","text":"
    def switch_screen(self, screen):\n

    Switch to another screen by replacing the top of the screen stack with a new screen.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    "},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen method","text":"
    def uninstall_screen(self, screen):\n

    Uninstall a screen.

    If the screen was not previously installed then this method is a null-op. Uninstalling a screen allows Textual to delete it when it is popped or switched. Note that uninstalling a screen is only required if you have previously installed it with install_screen. Textual will also uninstall screens automatically on exit.

    Parameters Parameter Default Description screen Screen | str required

    The screen to uninstall or the name of a installed screen.

    Returns Type Description str | None

    The name of the screen that was uninstalled, or None if no screen was uninstalled.

    "},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles method","text":"
    def update_styles(self, node):\n

    Immediately update the styles of this node and all descendant nodes.

    Should be called whenever CSS classes / pseudo classes change. For example, when you hover over a button, the :hover pseudo class will be added, and this method is called to apply the corresponding :hover styles.

    "},{"location":"api/app/#textual.app.App.validate_sub_title","title":"validate_sub_title method","text":"
    def validate_sub_title(self, sub_title):\n

    Make sure the sub-title is set to a string.

    "},{"location":"api/app/#textual.app.App.validate_title","title":"validate_title method","text":"
    def validate_title(self, title):\n

    Make sure the title is set to a string.

    "},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark method","text":"
    def watch_dark(self, dark):\n

    Watches the dark bool.

    This method handles the transition between light and dark mode when you change the dark attribute.

    "},{"location":"api/app/#textual.app.AppError","title":"AppError class","text":"

    Bases: Exception

    Base class for general App related exceptions.

    "},{"location":"api/app/#textual.app.InvalidModeError","title":"InvalidModeError class","text":"

    Bases: ModeError

    Raised if there is an issue with a mode name.

    "},{"location":"api/app/#textual.app.ModeError","title":"ModeError class","text":"

    Bases: Exception

    Base class for exceptions related to modes.

    "},{"location":"api/app/#textual.app.ScreenError","title":"ScreenError class","text":"

    Bases: Exception

    Base class for exceptions that relate to screens.

    "},{"location":"api/app/#textual.app.ScreenStackError","title":"ScreenStackError class","text":"

    Bases: ScreenError

    Raised when trying to manipulate the screen stack incorrectly.

    "},{"location":"api/app/#textual.app.UnknownModeError","title":"UnknownModeError class","text":"

    Bases: ModeError

    Raised when attempting to use a mode that is not known.

    "},{"location":"api/app/#textual.app.get_system_commands","title":"get_system_commands function","text":"
    def get_system_commands():\n

    Callable to lazy load the system commands.

    Returns Type Description type[SystemCommands]

    System commands class.

    "},{"location":"api/await_complete/","title":"Await complete","text":""},{"location":"api/await_complete/#textual.await_complete.AwaitComplete","title":"AwaitComplete class","text":"
    def __init__(self, *coroutines):\n

    An 'optionally-awaitable' object.

    Parameters Parameter Default Description coroutines Coroutine[Any, Any, Any] ()

    One or more coroutines to execute.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.exception","title":"exception property","text":"
    exception: BaseException | None\n

    An exception if it occurred in any of the coroutines.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.is_done","title":"is_done property","text":"
    is_done: bool\n

    Returns True if the task has completed.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.nothing","title":"nothing classmethod","text":"
    def nothing(cls):\n

    Returns an already completed instance of AwaitComplete.

    "},{"location":"api/await_remove/","title":"Await remove","text":"

    An optionally awaitable object returned by methods that remove widgets.

    "},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove class","text":"
    def __init__(self, finished_flag, task):\n

    An awaitable returned by a method that removes DOM nodes.

    Returned by Widget.remove and DOMQuery.remove.

    Parameters Parameter Default Description finished_flag Event required

    The asyncio event to wait on.

    task Task required

    The task which does the remove (required to keep a reference).

    "},{"location":"api/binding/","title":"Binding","text":"

    A binding maps a key press on to an action.

    See bindings in the guide for details.

    "},{"location":"api/binding/#textual.binding.Binding","title":"Binding class","text":"

    The configuration of a key binding.

    "},{"location":"api/binding/#textual.binding.Binding.action","title":"action instance-attribute","text":"
    action: str\n

    Action to bind to.

    "},{"location":"api/binding/#textual.binding.Binding.description","title":"description instance-attribute class-attribute","text":"
    description: str = ''\n

    Description of action.

    "},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
    key: str\n

    Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.

    "},{"location":"api/binding/#textual.binding.Binding.key_display","title":"key_display instance-attribute class-attribute","text":"
    key_display: str | None = None\n

    How the key should be shown in footer.

    "},{"location":"api/binding/#textual.binding.Binding.priority","title":"priority instance-attribute class-attribute","text":"
    priority: bool = False\n

    Enable priority binding for this key.

    "},{"location":"api/binding/#textual.binding.Binding.show","title":"show instance-attribute class-attribute","text":"
    show: bool = True\n

    Show the action in Footer, or False to hide.

    "},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError class","text":"

    Bases: Exception

    A binding related error.

    "},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding class","text":"

    Bases: Exception

    Binding key is in an invalid format.

    "},{"location":"api/binding/#textual.binding.NoBinding","title":"NoBinding class","text":"

    Bases: Exception

    A binding was not found.

    "},{"location":"api/color/","title":"Color","text":"

    This module contains a powerful Color class which Textual uses to manipulate colors.

    "},{"location":"api/color/#textual.color--named-colors","title":"Named colors","text":"

    The following named colors are used by the parse method.

    colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"api/color/#textual.color.BLACK","title":"BLACK module-attribute","text":"
    BLACK: Final = Color(0, 0, 0)\n

    A constant for pure black.

    "},{"location":"api/color/#textual.color.WHITE","title":"WHITE module-attribute","text":"
    WHITE: Final = Color(255, 255, 255)\n

    A constant for pure white.

    "},{"location":"api/color/#textual.color.Color","title":"Color class","text":"

    Bases: NamedTuple

    A class to represent a color.

    Colors are stored as three values representing the degree of red, green, and blue in a color, and a fourth \"alpha\" value which defines where the color lies on a gradient of opaque to transparent.

    Example
    >>> from textual.color import Color\n>>> color = Color.parse(\"red\")\n>>> color\nColor(255, 0, 0)\n>>> color.darken(0.5)\nColor(98, 0, 0)\n>>> color + Color.parse(\"green\")\nColor(0, 128, 0)\n>>> color_with_alpha = Color(100, 50, 25, 0.5)\n>>> color_with_alpha\nColor(100, 50, 25, a=0.5)\n>>> color + color_with_alpha\nColor(177, 25, 12)\n
    "},{"location":"api/color/#textual.color.Color.a","title":"a instance-attribute class-attribute","text":"
    a: float = 1.0\n

    Alpha (opacity) component in range 0 to 1.

    "},{"location":"api/color/#textual.color.Color.b","title":"b instance-attribute","text":"
    b: int\n

    Blue component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
    brightness: float\n

    The human perceptual brightness.

    A value of 1 is returned for pure white, and 0 for pure black. Other colors lie on a gradient between the two extremes.

    "},{"location":"api/color/#textual.color.Color.clamped","title":"clamped property","text":"
    clamped: Color\n

    A clamped color (this color with all values in expected range).

    "},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
    css: str\n

    The color in CSS RGB or RGBA form.

    For example, \"rgb(10,20,30)\" for an RGB color, or \"rgb(50,70,80,0.5)\" for an RGBA color.

    "},{"location":"api/color/#textual.color.Color.g","title":"g instance-attribute","text":"
    g: int\n

    Green component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
    hex: str\n

    The color in CSS hex form, with 6 digits for RGB, and 8 digits for RGBA.

    For example, \"#46b3de\" for an RGB color, or \"#3342457f\" for a color with alpha.

    "},{"location":"api/color/#textual.color.Color.hex6","title":"hex6 property","text":"
    hex6: str\n

    The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.

    For example, \"#46b3de\".

    "},{"location":"api/color/#textual.color.Color.hsl","title":"hsl property","text":"
    hsl: HSL\n

    This color in HSL format.

    HSL color is an alternative way of representing a color, which can be used in certain color calculations.

    Returns Type Description HSL

    Color encoded in HSL format.

    "},{"location":"api/color/#textual.color.Color.inverse","title":"inverse property","text":"
    inverse: Color\n

    The inverse of this color.

    Returns Type Description Color

    Inverse color.

    "},{"location":"api/color/#textual.color.Color.is_transparent","title":"is_transparent property","text":"
    is_transparent: bool\n

    Is the color transparent (i.e. has 0 alpha)?

    "},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
    monochrome: Color\n

    A monochrome version of this color.

    Returns Type Description Color

    The monochrome (black and white) version of this color.

    "},{"location":"api/color/#textual.color.Color.normalized","title":"normalized property","text":"
    normalized: tuple[float, float, float]\n

    A tuple of the color components normalized to between 0 and 1.

    Returns Type Description tuple[float, float, float]

    Normalized components.

    "},{"location":"api/color/#textual.color.Color.r","title":"r instance-attribute","text":"
    r: int\n

    Red component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.rgb","title":"rgb property","text":"
    rgb: tuple[int, int, int]\n

    The red, green, and blue color components as a tuple of ints.

    "},{"location":"api/color/#textual.color.Color.rich_color","title":"rich_color property","text":"
    rich_color: RichColor\n

    This color encoded in Rich's Color class.

    Returns Type Description RichColor

    A color object as used by Rich.

    "},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
    def blend(self, destination, factor, alpha=None):\n

    Generate a new color between two colors.

    This method calculates a new color on a gradient. The position on the gradient is given by factor, which is a float between 0 and 1, where 0 is the original color, and 1 is the destination color. A value of gradient between the two extremes produces a color somewhere between the two end points.

    Parameters Parameter Default Description destination Color required

    Another color.

    factor float required

    A blend factor, 0 -> 1.

    alpha float | None None

    New alpha for result.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.darken","title":"darken cached","text":"
    def darken(self, amount, alpha=None):\n

    Darken the color by a given amount.

    Parameters Parameter Default Description amount float required

    Value between 0-1 to reduce luminance by.

    alpha float | None None

    Alpha component for new color or None to copy alpha.

    Returns Type Description Color

    New color.

    "},{"location":"api/color/#textual.color.Color.from_hsl","title":"from_hsl classmethod","text":"
    def from_hsl(cls, h, s, l):\n

    Create a color from HLS components.

    Parameters Parameter Default Description h float required

    Hue.

    l float required

    Lightness.

    s float required

    Saturation.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.from_rich_color","title":"from_rich_color classmethod","text":"
    def from_rich_color(cls, rich_color):\n

    Create a new color from Rich's Color class.

    Parameters Parameter Default Description rich_color RichColor required

    An instance of Rich color.

    Returns Type Description Color

    A new Color instance.

    "},{"location":"api/color/#textual.color.Color.get_contrast_text","title":"get_contrast_text cached","text":"
    def get_contrast_text(self, alpha=0.95):\n

    Get a light or dark color that best contrasts this color, for use with text.

    Parameters Parameter Default Description alpha float 0.95

    An alpha value to apply to the result.

    Returns Type Description Color

    A new color, either an off-white or off-black.

    "},{"location":"api/color/#textual.color.Color.lighten","title":"lighten method","text":"
    def lighten(self, amount, alpha=None):\n

    Lighten the color by a given amount.

    Parameters Parameter Default Description amount float required

    Value between 0-1 to increase luminance by.

    alpha float | None None

    Alpha component for new color or None to copy alpha.

    Returns Type Description Color

    New color.

    "},{"location":"api/color/#textual.color.Color.multiply_alpha","title":"multiply_alpha method","text":"
    def multiply_alpha(self, alpha):\n

    Create a new color, multiplying the alpha by a constant.

    Parameters Parameter Default Description alpha float required

    A value to multiple the alpha by (expected to be in the range 0 to 1).

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.parse","title":"parse classmethod cached","text":"
    def parse(cls, color_text):\n

    Parse a string containing a named color or CSS-style color.

    Colors may be parsed from the following formats:

    • Text beginning with a # is parsed as a hexadecimal color code, where R, G, B, and A must be hexadecimal digits (0-9A-F):

      • #RGB
      • #RGBA
      • #RRGGBB
      • #RRGGBBAA
    • Alternatively, RGB colors can also be specified in the format that follows, where R, G, and B must be numbers between 0 and 255 and A must be a value between 0 and 1:

      • rgb(R,G,B)
      • rgb(R,G,B,A)
    • The HSL model can also be used, with a syntax similar to the above, if H is a value between 0 and 360, S and L are percentages, and A is a value between 0 and 1:

      • hsl(H,S,L)
      • hsla(H,S,L,A)

    Any other formats will raise a ColorParseError.

    Parameters Parameter Default Description color_text str | Color required

    Text with a valid color format. Color objects will be returned unmodified.

    Raises Type Description ColorParseError

    If the color is not encoded correctly.

    Returns Type Description Color

    Instance encoding the color specified by the argument.

    "},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha method","text":"
    def with_alpha(self, alpha):\n

    Create a new color with the given alpha.

    Parameters Parameter Default Description alpha float required

    New value for alpha.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError class","text":"
    def __init__(self, message, suggested_color=None):\n

    Bases: Exception

    A color failed to parse.

    Parameters Parameter Default Description message str required

    The error message

    suggested_color str | None None

    A close color we can suggest.

    "},{"location":"api/color/#textual.color.Gradient","title":"Gradient class","text":"
    def __init__(self, *stops):\n

    Defines a color gradient.

    A gradient is defined by a sequence of \"stops\" consisting of a float and a color. The stop indicate the color at that point on a spectrum between 0 and 1.

    Parameters Parameter Default Description stops tuple[float, Color] ()

    A colors stop.

    Raises Type Description ValueError

    If any stops are missing (must be at least a stop for 0 and 1).

    "},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color method","text":"
    def get_color(self, position):\n

    Get a color from the gradient at a position between 0 and 1.

    Positions that are between stops will return a blended color.

    Parameters Parameter Default Description position float required

    A number between 0 and 1, where 0 is the first stop, and 1 is the last.

    Returns Type Description Color

    A color.

    "},{"location":"api/color/#textual.color.HSL","title":"HSL class","text":"

    Bases: NamedTuple

    A color in HLS (Hue, Saturation, Lightness) format.

    "},{"location":"api/color/#textual.color.HSL.css","title":"css property","text":"
    css: str\n

    HSL in css format.

    "},{"location":"api/color/#textual.color.HSL.h","title":"h instance-attribute","text":"
    h: float\n

    Hue in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSL.l","title":"l instance-attribute","text":"
    l: float\n

    Lightness in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSL.s","title":"s instance-attribute","text":"
    s: float\n

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV","title":"HSV class","text":"

    Bases: NamedTuple

    A color in HSV (Hue, Saturation, Value) format.

    "},{"location":"api/color/#textual.color.HSV.h","title":"h instance-attribute","text":"
    h: float\n

    Hue in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.s","title":"s instance-attribute","text":"
    s: float\n

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
    v: float\n

    Value un range 0 to 1.

    "},{"location":"api/color/#textual.color.Lab","title":"Lab class","text":"

    Bases: NamedTuple

    A color in CIE-L*ab format.

    "},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
    L: float\n

    Lightness in range 0 to 100.

    "},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
    a: float\n

    A axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.Lab.b","title":"b instance-attribute","text":"
    b: float\n

    B axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb function","text":"
    def lab_to_rgb(lab, alpha=1.0):\n

    Convert a CIE-L*ab color to RGB.

    Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

    "},{"location":"api/color/#textual.color.rgb_to_lab","title":"rgb_to_lab function","text":"
    def rgb_to_lab(rgb):\n

    Convert an RGB color to the CIE-L*ab format.

    Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

    "},{"location":"api/command/","title":"Command","text":"

    The Textual command palette.

    See the guide on the Command Palette for full details.

    "},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
    Hits: TypeAlias = AsyncIterator[Hit]\n

    Return type for the command provider's search method.

    "},{"location":"api/command/#textual.command.Command","title":"Command class","text":"
    def __init__(self, prompt, command, id=None, disabled=False):\n

    Bases: Option

    Class that holds a command in the CommandList.

    Parameters Parameter Default Description prompt RenderableType required

    The prompt for the option.

    command Hit required

    The details of the command associated with the option.

    id str | None None

    The optional ID for the option.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"api/command/#textual.command.Command.command","title":"command instance-attribute","text":"
    command = command\n

    The details of the command associated with the option.

    "},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput class","text":"

    Bases: Input

    The command palette input control.

    "},{"location":"api/command/#textual.command.CommandList","title":"CommandList class","text":"

    Bases: OptionList

    The command palette command list.

    "},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette class","text":"
    def __init__(self):\n

    Bases: _SystemModalScreen[CallbackType]

    The Textual command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"ctrl+end, shift+end\",\n        \"command_list('last')\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+home, shift+home\",\n        \"command_list('first')\",\n        show=False,\n    ),\n    Binding(\"down\", \"cursor_down\", show=False),\n    Binding(\"escape\", \"escape\", \"Exit the command palette\"),\n    Binding(\n        \"pagedown\", \"command_list('page_down')\", show=False\n    ),\n    Binding(\n        \"pageup\", \"command_list('page_up')\", show=False\n    ),\n    Binding(\"up\", \"command_list('cursor_up')\", show=False),\n]\n
    Key(s) Description ctrl+end, shift+end Jump to the last available commands. ctrl+home, shift+home Jump to the first available commands. down Navigate down through the available commands. escape Exit the command palette. pagedown Navigate down a page through the available commands. pageup Navigate up a page through the available commands. up Navigate up through the available commands."},{"location":"api/command/#textual.command.CommandPalette.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"command-palette--help-text\",\n    \"command-palette--highlight\",\n}\n
    Class Description command-palette--help-text Targets the help text of a matched command. command-palette--highlight Targets the highlights of a matched command."},{"location":"api/command/#textual.command.CommandPalette.run_on_select","title":"run_on_select class-attribute","text":"
    run_on_select: bool = True\n

    A flag to say if a command should be run when selected by the user.

    If True then when a user hits Enter on a command match in the result list, or if they click on one with the mouse, the command will be selected and run. If set to False the input will be filled with the command and then Enter should be pressed on the keyboard or the 'go' button should be pressed.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
    def is_open(app):\n

    Is the command palette current open?

    Parameters Parameter Default Description app App required

    The app to test.

    Returns Type Description bool

    True if the command palette is currently open, False if not.

    "},{"location":"api/command/#textual.command.Hit","title":"Hit class","text":"

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
    command: IgnoreReturnCallbackType\n

    The function to call when the command is chosen.

    "},{"location":"api/command/#textual.command.Hit.help","title":"help instance-attribute class-attribute","text":"
    help: str | None = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
    match_display: RenderableType\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
    score: float\n

    The score of the command hit.

    The value should be between 0 (no match) and 1 (complete match).

    "},{"location":"api/command/#textual.command.Hit.text","title":"text instance-attribute class-attribute","text":"
    text: str | None = None\n

    The command text associated with the hit, as plain text.

    If match_display is not simple text, this attribute should be provided by the Provider object.

    "},{"location":"api/command/#textual.command.Matcher","title":"Matcher class","text":"
    def __init__(\n    self, query, *, match_style=None, case_sensitive=False\n):\n

    A fuzzy matcher.

    Parameters Parameter Default Description query str required

    A query as typed in by the user.

    match_style Style | None None

    The style to use to highlight matched portions of a string.

    case_sensitive bool False

    Should matching be case sensitive?

    "},{"location":"api/command/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive: bool\n

    Is this matcher case sensitive?

    "},{"location":"api/command/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_style: Style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/command/#textual.fuzzy.Matcher.query","title":"query property","text":"
    query: str\n

    The query string to look for.

    "},{"location":"api/command/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern: str\n

    The regular expression pattern built from the query.

    "},{"location":"api/command/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
    def highlight(self, candidate):\n

    Highlight the candidate with the fuzzy match.

    Parameters Parameter Default Description candidate str required

    The candidate string to match against the query.

    Returns Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/command/#textual.fuzzy.Matcher.match","title":"match method","text":"
    def match(self, candidate):\n

    Match the candidate against the query.

    Parameters Parameter Default Description candidate str required

    Candidate string to match against the query.

    Returns Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/command/#textual.command.Provider","title":"Provider class","text":"
    def __init__(self, screen, match_style=None):\n

    Bases: ABC

    Base class for command palette command providers.

    To create new command provider, inherit from this class and implement search.

    Parameters Parameter Default Description screen Screen[Any] required

    A reference to the active screen.

    "},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
    app: App[object]\n

    A reference to the application.

    "},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
    focused: Widget | None\n

    The currently-focused widget in the currently-active screen in the application.

    If no widget has focus this will be None.

    "},{"location":"api/command/#textual.command.Provider.match_style","title":"match_style property","text":"
    match_style: Style | None\n

    The preferred style to use when highlighting matching portions of the match_display.

    "},{"location":"api/command/#textual.command.Provider.screen","title":"screen property","text":"
    screen: Screen[object]\n

    The currently-active screen in the application.

    "},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher method","text":"
    def matcher(self, user_input, case_sensitive=False):\n

    Create a fuzzy matcher for the given user input.

    Parameters Parameter Default Description user_input str required

    The text that the user has input.

    case_sensitive bool False

    Should matching be case sensitive?

    Returns Type Description Matcher

    A fuzzy matcher object for matching against candidate hits.

    "},{"location":"api/command/#textual.command.Provider.search","title":"search abstractmethod async","text":"
    def search(self, query):\n

    A request to search for commands relevant to the given query.

    Parameters Parameter Default Description query str required

    The user input to be matched.

    Yields:

    Type Description Hits

    Instances of Hit.

    "},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
    def shutdown(self):\n

    Called when the Provider is shutdown.

    Use this method to perform an cleanup, if required.

    "},{"location":"api/command/#textual.command.Provider.startup","title":"startup async","text":"
    def startup(self):\n

    Called after the Provider is initialized, but before any calls to search.

    "},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon class","text":"

    Bases: Static

    Widget for displaying a search icon before the command input.

    "},{"location":"api/command/#textual.command.SearchIcon.icon","title":"icon instance-attribute class-attribute","text":"
    icon: var[str] = var(\n    Emoji.replace(\":magnifying_glass_tilted_right:\")\n)\n

    The icon to display.

    "},{"location":"api/containers/","title":"Containers","text":"

    Container widgets for quick styling.

    With the exception of Center and Middle containers will fill all of the space in the parent widget.

    "},{"location":"api/containers/#textual.containers.Center","title":"Center class","text":"

    Bases: Widget

    A container which aligns children on the X axis.

    "},{"location":"api/containers/#textual.containers.Container","title":"Container class","text":"

    Bases: Widget

    Simple container widget, with vertical layout.

    "},{"location":"api/containers/#textual.containers.Grid","title":"Grid class","text":"

    Bases: Widget

    A container with grid layout.

    "},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal class","text":"

    Bases: Widget

    A container with horizontal layout and no scrollbars.

    "},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll class","text":"

    Bases: ScrollableContainer

    A container with horizontal layout and an automatic scrollbar on the Y axis.

    "},{"location":"api/containers/#textual.containers.Middle","title":"Middle class","text":"

    Bases: Widget

    A container which aligns children on the Y axis.

    "},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer class","text":"

    Bases: Widget

    A scrollable container with vertical layout, and auto scrollbars on both axis.

    "},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"up\", \"scroll_up\", \"Scroll Up\", show=False),\n    Binding(\n        \"down\", \"scroll_down\", \"Scroll Down\", show=False\n    ),\n    Binding(\"left\", \"scroll_left\", \"Scroll Up\", show=False),\n    Binding(\n        \"right\", \"scroll_right\", \"Scroll Right\", show=False\n    ),\n    Binding(\n        \"home\", \"scroll_home\", \"Scroll Home\", show=False\n    ),\n    Binding(\"end\", \"scroll_end\", \"Scroll End\", show=False),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n]\n

    Keyboard bindings for scrollable containers.

    Key(s) Description up Scroll up, if vertical scrolling is available. down Scroll down, if vertical scrolling is available. left Scroll left, if horizontal scrolling is available. right Scroll right, if horizontal scrolling is available. home Scroll to the home position, if scrolling is available. end Scroll to the end position, if scrolling is available. pageup Scroll up one page, if vertical scrolling is available. pagedown Scroll down one page, if vertical scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical class","text":"

    Bases: Widget

    A container with vertical layout and no scrollbars.

    "},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll class","text":"

    Bases: ScrollableContainer

    A container with vertical layout and an automatic scrollbar on the Y axis.

    "},{"location":"api/containers/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current instance-attribute class-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"api/content_switcher/","title":"Content switcher","text":""},{"location":"api/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current instance-attribute class-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"api/coordinate/","title":"Coordinate","text":"

    A class to store a coordinate, used by the DataTable.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate class","text":"

    Bases: NamedTuple

    An object representing a row/column coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.column","title":"column instance-attribute","text":"
    column: int\n

    The column of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
    row: int\n

    The row of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down method","text":"
    def down(self):\n

    Get the coordinate below.

    Returns Type Description Coordinate

    The coordinate below.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left method","text":"
    def left(self):\n

    Get the coordinate to the left.

    Returns Type Description Coordinate

    The coordinate to the left.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.right","title":"right method","text":"
    def right(self):\n

    Get the coordinate to the right.

    Returns Type Description Coordinate

    The coordinate to the right.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.up","title":"up method","text":"
    def up(self):\n

    Get the coordinate above.

    Returns Type Description Coordinate

    The coordinate above.

    "},{"location":"api/dom_node/","title":"Dom node","text":"

    A DOMNode is a base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

    "},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
    WalkMethod: TypeAlias = Literal['depth', 'breadth']\n

    Valid walking methods for the DOMNode.walk_children method.

    "},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier class","text":"

    Bases: Exception

    Exception raised if you supply a id attribute or class name in the wrong format.

    "},{"location":"api/dom_node/#textual.dom.DOMError","title":"DOMError class","text":"

    Bases: Exception

    Base exception class for errors relating to the DOM.

    "},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode class","text":"
    def __init__(self, *, name=None, id=None, classes=None):\n

    Bases: MessagePump

    The base class for object that can be in the Textual DOM (App and Widget)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
    SCOPED_CSS: bool = True\n

    Should default css be limited to the widget type?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
    ancestors: list[DOMNode]\n

    A list of ancestor nodes found by tracing a path all the way back to App.

    Returns Type Description list[DOMNode]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors_with_self","title":"ancestors_with_self property","text":"
    ancestors_with_self: list[DOMNode]\n

    A list of ancestor nodes found by tracing a path all the way back to App.

    Note

    This is inclusive of self.

    Returns Type Description list[DOMNode]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.auto_refresh","title":"auto_refresh property writable","text":"
    auto_refresh: float | None\n

    Number of seconds between automatic refresh, or None for no automatic refresh.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.background_colors","title":"background_colors property","text":"
    background_colors: tuple[Color, Color]\n

    The background color and the color of the parent's background.

    Returns Type Description tuple[Color, Color]

    (<background color>, <color>)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.children","title":"children property","text":"
    children: Sequence['Widget']\n

    A view on to the children.

    Returns Type Description Sequence['Widget']

    The node's children.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.classes","title":"classes instance-attribute class-attribute","text":"
    classes = _ClassesDescriptor()\n

    CSS class names for this node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.colors","title":"colors property","text":"
    colors: tuple[Color, Color, Color, Color]\n

    The widget's background and foreground colors, and the parent's background and foreground colors.

    Returns Type Description tuple[Color, Color, Color, Color]

    (<parent background>, <parent color>, <background>, <color>)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier","title":"css_identifier property","text":"
    css_identifier: str\n

    A CSS selector that identifies this DOM node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier_styled","title":"css_identifier_styled property","text":"
    css_identifier_styled: Text\n

    A syntax highlighted CSS identifier.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_path_nodes","title":"css_path_nodes property","text":"
    css_path_nodes: list[DOMNode]\n

    A list of nodes from the App to this node, forming a \"path\".

    Returns Type Description list[DOMNode]

    A list of nodes, where the first item is the App, and the last is this node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_tree","title":"css_tree property","text":"
    css_tree: Tree\n

    A Rich tree to display the DOM, annotated with the node's CSS.

    Log this to visualize your app in the textual console.

    Example
    self.log(self.css_tree)\n
    Returns Type Description Tree

    A Tree renderable.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.display","title":"display property writable","text":"
    display: bool\n

    Should the DOM node be displayed?

    May be set to a boolean to show or hide the node, or to any valid value for the display rule.

    Example
    my_widget.display = False  # Hide my_widget\n
    "},{"location":"api/dom_node/#textual.dom.DOMNode.displayed_children","title":"displayed_children property","text":"
    displayed_children: list[Widget]\n

    The child nodes which will be displayed.

    Returns Type Description list[Widget]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.id","title":"id property writable","text":"
    id: str | None\n

    The ID of this node, or None if the node has no ID.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.is_modal","title":"is_modal property","text":"
    is_modal: bool\n

    Is the node a modal?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
    name: str | None\n

    The name of the node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
    parent: DOMNode | None\n

    The parent node.

    All nodes have parent once added to the DOM, with the exception of the App which is the root node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.pseudo_classes","title":"pseudo_classes property","text":"
    pseudo_classes: frozenset[str]\n

    A (frozen) set of all pseudo classes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
    rich_style: Style\n

    Get a Rich Style object for this DOMNode.

    Returns Type Description Style

    A Rich style.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.screen","title":"screen property","text":"
    screen: 'Screen[object]'\n

    The screen containing this node.

    Returns Type Description 'Screen[object]'

    A screen object.

    Raises Type Description NoScreen

    If this node isn't mounted (and has no screen).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.text_style","title":"text_style property","text":"
    text_style: Style\n

    Get the text style object.

    A widget's style is influenced by its parent. for instance if a parent is bold, then the child will also be bold.

    Returns Type Description Style

    A Rich Style.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.tree","title":"tree property","text":"
    tree: Tree\n

    A Rich tree to display the DOM.

    Log this to visualize your app in the textual console.

    Example
    self.log(self.tree)\n
    Returns Type Description Tree

    A Tree renderable.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.visible","title":"visible property writable","text":"
    visible: bool\n

    Is this widget visible in the DOM?

    If a widget hasn't had its visibility set explicitly, then it inherits it from its DOM ancestors.

    This may be set explicitly to override inherited values. The valid values include the valid values for the visibility rule and the booleans True or False, to set the widget to be visible or invisible, respectively.

    When a node is invisible, Textual will reserve space for it, but won't display anything.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.workers","title":"workers property","text":"
    workers: WorkerManager\n

    The app's worker manager. Shortcut for self.app.workers.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class method","text":"
    def add_class(self, *class_names, update=True):\n

    Add class names to this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to add.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles method","text":"
    def get_component_styles(self, name):\n

    Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

    Parameters Parameter Default Description name str required

    Name of the component.

    Raises Type Description KeyError

    If the component class doesn't exist.

    Returns Type Description RenderStyles

    A Styles object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes method","text":"
    def get_pseudo_classes(self):\n

    Get any pseudo classes applicable to this Node, e.g. hover, focus.

    Returns Type Description Iterable[str]

    Iterable of strings, such as a generator.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_class","title":"has_class method","text":"
    def has_class(self, *class_names):\n

    Check if the Node has all the given class names.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to check.

    Returns Type Description bool

    True if the node has all the given class names, otherwise False.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class method","text":"
    def has_pseudo_class(self, *class_names):\n

    Check for pseudo classes (such as hover, focus etc)

    Parameters Parameter Default Description *class_names str ()

    The pseudo classes to check for.

    Returns Type Description bool

    True if the DOM node has those pseudo classes, False if not.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update method","text":"
    def notify_style_update(self):\n

    Called after styles are updated.

    Implement this in a subclass if you want to clear any cached data when the CSS is reloaded.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query","title":"query method","text":"
    def query(self, selector=None):\n

    Get a DOM query matching a selector.

    Parameters Parameter Default Description selector str | type[QueryType] | None None

    A CSS selector or None for all nodes.

    Returns Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one method","text":"
    def query_one(self, selector, expect_type=None):\n

    Get a single Widget matching the given selector or selector type.

    Parameters Parameter Default Description selector str | type[QueryType] required

    A selector.

    expect_type type[QueryType] | None None

    Require the object be of the supplied type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    TooManyMatches

    If there is more than one matching node in the query.

    Returns Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class method","text":"
    def remove_class(self, *class_names, update=True):\n

    Remove class names from this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to remove.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles method","text":"
    def reset_styles(self):\n

    Reset styles back to their initial state.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker method","text":"
    def run_worker(\n    self,\n    work,\n    name=\"\",\n    group=\"default\",\n    description=\"\",\n    exit_on_error=True,\n    start=True,\n    exclusive=False,\n    thread=False,\n):\n

    Run work in a worker.

    A worker runs a function, coroutine, or awaitable, in the background as an async task or as a thread.

    Parameters Parameter Default Description work WorkType[ResultType] required

    A function, async function, or an awaitable object to run in a worker.

    name str | None ''

    A short string to identify the worker (in logs and debugging).

    group str 'default'

    A short string to identify a group of workers.

    description str ''

    A longer string to store longer information on the worker.

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    start bool True

    Start the worker immediately.

    exclusive bool False

    Cancel all workers in the same group.

    thread bool False

    Mark the worker as a thread worker.

    Returns Type Description Worker[ResultType]

    New Worker instance.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class method","text":"
    def set_class(self, add, *class_names, update=True):\n

    Add or remove class(es) based on a condition.

    Parameters Parameter Default Description add bool required

    Add the classes if True, otherwise remove them.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes method","text":"
    def set_classes(self, classes):\n

    Replace all classes.

    Parameters Parameter Default Description classes str | Iterable[str] required

    A string containing space separated classes, or an iterable of class names.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles method","text":"
    def set_styles(self, css=None, **update_styles):\n

    Set custom styles on this object.

    Parameters Parameter Default Description css str | None None

    Styles in CSS format.

    update_styles Any {}

    Keyword arguments map style names onto style values.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class method","text":"
    def toggle_class(self, *class_names):\n

    Toggle class names on this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to toggle.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children method","text":"
    def walk_children(\n    self,\n    filter_type=None,\n    *,\n    with_self=False,\n    method=\"depth\",\n    reverse=False\n):\n

    Walk the subtree rooted at this node, and return every descendant encountered in a list.

    Parameters Parameter Default Description filter_type type[WalkType] | None None

    Filter only this type, or None for no filter.

    with_self bool False

    Also yield self in addition to descendants.

    method WalkMethod 'depth'

    One of \"depth\" or \"breadth\".

    reverse bool False

    Reverse the order (bottom up).

    Returns Type Description list[DOMNode] | list[WalkType]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch method","text":"
    def watch(self, obj, attribute_name, callback, init=True):\n

    Watches for modifications to reactive attributes on another object.

    Example

    Here's how you could detect when the app changes from dark to light mode (and vice versa).

    def on_dark_change(old_value:bool, new_value:bool):\n    # Called when app.dark changes.\n    print(\"App.dark when from {old_value} to {new_value}\")\n\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n
    Parameters Parameter Default Description obj DOMNode required

    Object containing attribute to watch.

    attribute_name str required

    Attribute to watch.

    callback WatchCallbackType required

    A callback to run when attribute changes.

    init bool True

    Check watchers on first call.

    "},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen class","text":"

    Bases: DOMError

    Raised when the node has no associated screen.

    "},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers function","text":"
    def check_identifiers(description, *names):\n

    Validate identifier and raise an error if it fails.

    Parameters Parameter Default Description description str required

    Description of where identifier is used for error message.

    *names str ()

    Identifiers to check.

    "},{"location":"api/errors/","title":"Errors","text":"

    General exception classes.

    "},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers class","text":"

    Bases: TextualError

    More than one handler for a single key press.

    For example, if the handlers key_ctrl_i and key_tab were defined on the same widget, then this error would be raised.

    "},{"location":"api/errors/#textual.errors.NoWidget","title":"NoWidget class","text":"

    Bases: TextualError

    Specified widget was not found.

    "},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError class","text":"

    Bases: TextualError

    An object could not be rendered.

    "},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError class","text":"

    Bases: Exception

    Base class for Textual errors.

    "},{"location":"api/events/","title":"Events","text":"

    Builtin events sent by Textual.

    Events may be marked as \"Bubbles\" and \"Verbose\". See the events guide for an explanation of bubbling. Verbose events are excluded from the textual console, unless you explicitly request them with the -v switch as follows:

    textual console -v\n
    "},{"location":"api/events/#textual.events.AppBlur","title":"AppBlur class","text":"

    Bases: Event

    Sent when the app loses focus.

    Used by textual-web.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.AppFocus","title":"AppFocus class","text":"

    Bases: Event

    Sent when the app has focus.

    Used by textual-web.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Blur","title":"Blur class","text":"

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Click","title":"Click class","text":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Compose","title":"Compose class","text":"

    Bases: Event

    Sent to a widget to request it to compose and mount children.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur class","text":"

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
    control: Widget\n

    The widget that was blurred (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was blurred.

    "},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus class","text":"

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
    control: Widget\n

    The widget that was focused (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was focused.

    "},{"location":"api/events/#textual.events.Enter","title":"Enter class","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Event","title":"Event class","text":"

    Bases: Message

    The base class for all events.

    "},{"location":"api/events/#textual.events.Focus","title":"Focus class","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Hide","title":"Hide class","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

    "},{"location":"api/events/#textual.events.Idle","title":"Idle class","text":"

    Bases: Event

    Sent when there are no more items in the message queue.

    This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.InputEvent","title":"InputEvent class","text":"

    Bases: Event

    Base class for input events.

    "},{"location":"api/events/#textual.events.Key","title":"Key class","text":"
    def __init__(self, key, character):\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description key str required

    The key that was pressed.

    character str | None required

    A printable character or None if it is not printable.

    Attributes Name Type Description aliases list[str]

    The aliases for the key, including the key itself.

    "},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable: bool\n

    Check if the key is printable (produces a unicode character).

    Returns Type Description bool

    True if the key is printable.

    "},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
    name: str\n

    Name of a key suitable for use as a Python identifier.

    "},{"location":"api/events/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
    name_aliases: list[str]\n

    The corresponding name for every alias in aliases list.

    "},{"location":"api/events/#textual.events.Leave","title":"Leave class","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Load","title":"Load class","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Mount","title":"Mount class","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Sent when the mouse has been captured.

    • Bubbles
    • Verbose

    When a mouse has been captured, all further mouse events will be sent to the capturing widget.

    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when captured.

    "},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown class","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent class","text":"
    def __init__(\n    self,\n    x,\n    y,\n    delta_x,\n    delta_y,\n    button,\n    shift,\n    meta,\n    ctrl,\n    screen_x=None,\n    screen_y=None,\n    style=None,\n):\n

    Bases: InputEvent

    Sent in response to a mouse event.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description x int required

    The relative x coordinate.

    y int required

    The relative y coordinate.

    delta_x int required

    Change in x since the last message.

    delta_y int required

    Change in y since the last message.

    button int required

    Indexed of the pressed button.

    shift bool required

    True if the shift key is pressed.

    meta bool required

    True if the meta key is pressed.

    ctrl bool required

    True if the ctrl key is pressed.

    screen_x int | None None

    The absolute x coordinate.

    screen_y int | None None

    The absolute y coordinate.

    style Style | None None

    The Rich Style under the mouse cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
    delta: Offset\n

    Mouse coordinate delta (change since last event).

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
    offset: Offset\n

    The mouse coordinate as an offset.

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_offset","title":"screen_offset property","text":"
    screen_offset: Offset\n

    Mouse coordinate relative to the screen.

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.style","title":"style property writable","text":"
    style: Style\n

    The (Rich) Style under the cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset method","text":"
    def get_content_offset(self, widget):\n

    Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

    Parameters Parameter Default Description widget Widget required

    Widget receiving the event.

    Returns Type Description Offset | None

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture method","text":"
    def get_content_offset_capture(self, widget):\n

    Get offset from a widget's content area.

    This method works even if the offset is outside the widget content region.

    Parameters Parameter Default Description widget Widget required

    Widget receiving the event.

    Returns Type Description Offset

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove class","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when released.

    "},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp class","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Paste","title":"Paste class","text":"
    def __init__(self, text):\n

    Bases: Event

    Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    The text that has been pasted.

    "},{"location":"api/events/#textual.events.Print","title":"Print class","text":"
    def __init__(self, text, stderr=False):\n

    Bases: Event

    Sent to a widget that is capturing prints.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    Text that was printed.

    stderr bool False

    True if the print was to stderr, or False for stdout.

    "},{"location":"api/events/#textual.events.Ready","title":"Ready class","text":"

    Bases: Event

    Sent to the app when the DOM is ready.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Resize","title":"Resize class","text":"
    def __init__(self, size, virtual_size, container_size=None):\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description size Size required

    The new size of the Widget.

    virtual_size Size required

    The virtual size (scrollable size) of the Widget.

    container_size Size | None None

    The size of the Widget's container widget.

    "},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume class","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend class","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Show","title":"Show class","text":"

    Bases: Event

    Sent when a widget has become visible.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Timer","title":"Timer class","text":"
    def __init__(self, timer, time, count=0, callback=None):\n

    Bases: Event

    Sent in response to a timer.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Unmount","title":"Unmount class","text":"

    Bases: Event

    Sent when a widget is unmounted and may not longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/filter/","title":"Filter","text":"

    Filter classes.

    Note

    Filters are used internally, and not recommended for use by Textual app developers.

    Filters are used internally to process terminal output after it has been rendered. Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set.

    In the future, this system will be used to implement accessibility features.

    "},{"location":"api/filter/#textual.filter.NO_DIM","title":"NO_DIM module-attribute","text":"
    NO_DIM = Style(dim=False)\n

    A Style to set dim to False.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor","title":"ANSIToTruecolor class","text":"
    def __init__(self, terminal_theme):\n

    Bases: LineFilter

    Convert ANSI colors to their truecolor equivalents.

    Parameters Parameter Default Description terminal_theme TerminalTheme required

    A rich terminal theme.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
    def truecolor_style(self, style):\n

    Replace system colors with truecolor equivalent.

    Parameters Parameter Default Description style Style required

    Style to apply truecolor filter to.

    Returns Type Description Style

    New style.

    "},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter class","text":"
    def __init__(self, dim_factor=0.5):\n

    Bases: LineFilter

    Replace dim attributes with modified colors.

    Parameters Parameter Default Description dim_factor float 0.5

    The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

    "},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter class","text":"

    Bases: ABC

    Base class for a line filter.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome class","text":"

    Bases: LineFilter

    Convert all colors to monochrome.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
    def dim_color(background, color, factor):\n

    Dim a color by blending towards the background

    Parameters Parameter Default Description background RichColor required

    background color.

    color RichColor required

    Foreground color.

    factor float required

    Blend factor

    Returns Type Description RichColor

    New dimmer color.

    "},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
    def dim_style(style, background, factor):\n

    Replace dim attribute with a dim color.

    Parameters Parameter Default Description style Style required

    Style to dim.

    factor float required

    Blend factor.

    Returns Type Description Style

    New dimmed style.

    "},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
    def monochrome_style(style):\n

    Convert colors in a style to monochrome.

    Parameters Parameter Default Description style Style required

    A Rich Style.

    Returns Type Description Style

    A new Rich style.

    "},{"location":"api/fuzzy_matcher/","title":"Fuzzy matcher","text":"

    Fuzzy matcher.

    This class is used by the command palette to match search terms.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher class","text":"
    def __init__(\n    self, query, *, match_style=None, case_sensitive=False\n):\n

    A fuzzy matcher.

    Parameters Parameter Default Description query str required

    A query as typed in by the user.

    match_style Style | None None

    The style to use to highlight matched portions of a string.

    case_sensitive bool False

    Should matching be case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive: bool\n

    Is this matcher case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_style: Style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query","title":"query property","text":"
    query: str\n

    The query string to look for.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern: str\n

    The regular expression pattern built from the query.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
    def highlight(self, candidate):\n

    Highlight the candidate with the fuzzy match.

    Parameters Parameter Default Description candidate str required

    The candidate string to match against the query.

    Returns Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match method","text":"
    def match(self, candidate):\n

    Match the candidate against the query.

    Parameters Parameter Default Description candidate str required

    Candidate string to match against the query.

    Returns Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/geometry/","title":"Geometry","text":"

    Functions and classes to manage terminal geometry (anything involving coordinates or dimensions).

    "},{"location":"api/geometry/#textual.geometry.NULL_OFFSET","title":"NULL_OFFSET module-attribute","text":"
    NULL_OFFSET: Final = Offset(0, 0)\n

    An offset constant for (0, 0).

    "},{"location":"api/geometry/#textual.geometry.NULL_REGION","title":"NULL_REGION module-attribute","text":"
    NULL_REGION: Final = Region(0, 0, 0, 0)\n

    A Region constant for a null region (at the origin, with both width and height set to zero).

    "},{"location":"api/geometry/#textual.geometry.NULL_SIZE","title":"NULL_SIZE module-attribute","text":"
    NULL_SIZE: Final = Size(0, 0)\n

    A Size constant for a null size (with zero area).

    "},{"location":"api/geometry/#textual.geometry.NULL_SPACING","title":"NULL_SPACING module-attribute","text":"
    NULL_SPACING: Final = Spacing(0, 0, 0, 0)\n

    A Spacing constant for no space.

    "},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
    SpacingDimensions: TypeAlias = Union[\n    int,\n    Tuple[int],\n    Tuple[int, int],\n    Tuple[int, int, int, int],\n]\n

    The valid ways in which you can specify spacing.

    "},{"location":"api/geometry/#textual.geometry.Offset","title":"Offset class","text":"

    Bases: NamedTuple

    A cell offset defined by x and y coordinates.

    Offsets are typically relative to the top left of the terminal or other container.

    Textual prefers the names x and y, but you could consider x to be the column and y to be the row.

    Offsets support addition, subtraction, multiplication, and negation.

    Example
    >>> from textual.geometry import Offset\n>>> offset = Offset(3, 2)\n>>> offset\nOffset(x=3, y=2)\n>>> offset += Offset(10, 0)\n>>> offset\nOffset(x=13, y=2)\n>>> -offset\nOffset(x=-13, y=-2)\n
    "},{"location":"api/geometry/#textual.geometry.Offset.clamped","title":"clamped property","text":"
    clamped: Offset\n

    This offset with x and y restricted to values above zero.

    "},{"location":"api/geometry/#textual.geometry.Offset.is_origin","title":"is_origin property","text":"
    is_origin: bool\n

    Is the offset at (0, 0)?

    "},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x instance-attribute class-attribute","text":"
    x: int = 0\n

    Offset in the x-axis (horizontal)

    "},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y instance-attribute class-attribute","text":"
    y: int = 0\n

    Offset in the y-axis (vertical)

    "},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend method","text":"
    def blend(self, destination, factor):\n

    Calculate a new offset on a line between this offset and a destination offset.

    Parameters Parameter Default Description destination Offset required

    Point where factor would be 1.0.

    factor float required

    A value between 0 and 1.0.

    Returns Type Description Offset

    A new point on a line between self and destination.

    "},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to method","text":"
    def get_distance_to(self, other):\n

    Get the distance to another offset.

    Parameters Parameter Default Description other Offset required

    An offset.

    Returns Type Description float

    Distance to other offset.

    "},{"location":"api/geometry/#textual.geometry.Region","title":"Region class","text":"

    Bases: NamedTuple

    Defines a rectangular region.

    A Region consists of a coordinate (x and y) and dimensions (width and height).

      (x, y)\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u25b2\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 height\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u25bc\n    \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500 width \u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n
    Example
    >>> from textual.geometry import Region\n>>> region = Region(4, 5, 20, 10)\n>>> region\nRegion(x=4, y=5, width=20, height=10)\n>>> region.area\n200\n>>> region.size\nSize(width=20, height=10)\n>>> region.offset\nOffset(x=4, y=5)\n>>> region.contains(1, 2)\nFalse\n>>> region.contains(10, 8)\nTrue\n
    "},{"location":"api/geometry/#textual.geometry.Region.area","title":"area property","text":"
    area: int\n

    The area under the region.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
    bottom: int\n

    Maximum Y value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
    bottom_left: Offset\n

    Bottom left offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_right","title":"bottom_right property","text":"
    bottom_right: Offset\n

    Bottom right offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.center","title":"center property","text":"
    center: tuple[float, float]\n

    The center of the region.

    Note, that this does not return an Offset, because the center may not be an integer coordinate.

    Returns Type Description tuple[float, float]

    Tuple of floats.

    "},{"location":"api/geometry/#textual.geometry.Region.column_range","title":"column_range property","text":"
    column_range: range\n

    A range object for X coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
    column_span: tuple[int, int]\n

    A pair of integers for the start and end columns (x coordinates) in this region.

    The end value is exclusive.

    "},{"location":"api/geometry/#textual.geometry.Region.corners","title":"corners property","text":"
    corners: tuple[int, int, int, int]\n

    The top left and bottom right coordinates as a tuple of four integers.

    "},{"location":"api/geometry/#textual.geometry.Region.height","title":"height instance-attribute class-attribute","text":"
    height: int = 0\n

    The height of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
    line_range: range\n

    A range object for Y coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
    line_span: tuple[int, int]\n

    A pair of integers for the start and end lines (y coordinates) in this region.

    The end value is exclusive.

    "},{"location":"api/geometry/#textual.geometry.Region.offset","title":"offset property","text":"
    offset: Offset\n

    The top left corner of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.reset_offset","title":"reset_offset property","text":"
    reset_offset: Region\n

    An region of the same size at (0, 0).

    Returns Type Description Region

    A region at the origin.

    "},{"location":"api/geometry/#textual.geometry.Region.right","title":"right property","text":"
    right: int\n

    Maximum X value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
    size: Size\n

    Get the size of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
    top_right: Offset\n

    Top right offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.width","title":"width instance-attribute class-attribute","text":"
    width: int = 0\n

    The width of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.x","title":"x instance-attribute class-attribute","text":"
    x: int = 0\n

    Offset in the x-axis (horizontal).

    "},{"location":"api/geometry/#textual.geometry.Region.y","title":"y instance-attribute class-attribute","text":"
    y: int = 0\n

    Offset in the y-axis (vertical).

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset method","text":"
    def at_offset(self, offset):\n

    Get a new Region with the same size at a given offset.

    Parameters Parameter Default Description offset tuple[int, int] required

    An offset.

    Returns Type Description Region

    New Region with adjusted offset.

    "},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip method","text":"
    def clip(self, width, height):\n

    Clip this region to fit within width, height.

    Parameters Parameter Default Description width int required

    Width of bounds.

    height int required

    Height of bounds.

    Returns Type Description Region

    Clipped region.

    "},{"location":"api/geometry/#textual.geometry.Region.clip_size","title":"clip_size method","text":"
    def clip_size(self, size):\n

    Clip the size to fit within minimum values.

    Parameters Parameter Default Description size tuple[int, int] required

    Maximum width and height.

    Returns Type Description Region

    No region, not bigger than size.

    "},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains method","text":"
    def contains(self, x, y):\n

    Check if a point is in the region.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point method","text":"
    def contains_point(self, point):\n

    Check if a point is in the region.

    Parameters Parameter Default Description point tuple[int, int] required

    A tuple of x and y coordinates.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
    def contains_region(self, other):\n

    Check if a region is entirely contained within this region.

    Parameters Parameter Default Description other Region required

    A region.

    Returns Type Description bool

    True if the other region fits perfectly within this region.

    "},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size method","text":"
    def crop_size(self, size):\n

    Get a region with the same offset, with a size no larger than size.

    Parameters Parameter Default Description size tuple[int, int] required

    Maximum width and height (WIDTH, HEIGHT).

    Returns Type Description Region

    New region that could fit within size.

    "},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand method","text":"
    def expand(self, size):\n

    Increase the size of the region by adding a border.

    Parameters Parameter Default Description size tuple[int, int] required

    Additional width and height.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
    def from_corners(cls, x1, y1, x2, y2):\n

    Construct a Region form the top left and bottom right corners.

    Parameters Parameter Default Description x1 int required

    Top left x.

    y1 int required

    Top left y.

    x2 int required

    Bottom right x.

    y2 int required

    Bottom right y.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
    def from_offset(cls, offset, size):\n

    Create a region from offset and size.

    Parameters Parameter Default Description offset tuple[int, int] required

    Offset (top left point).

    size tuple[int, int] required

    Dimensions of region.

    Returns Type Description Region

    A region instance.

    "},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
    def from_union(cls, regions):\n

    Create a Region from the union of other regions.

    Parameters Parameter Default Description regions Collection[Region] required

    One or more regions.

    Returns Type Description Region

    A Region that encloses all other regions.

    "},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
    def get_scroll_to_visible(\n    cls, window_region, region, *, top=False\n):\n

    Calculate the smallest offset required to translate a window so that it contains another region.

    This method is used to calculate the required offset to scroll something in to view.

    Parameters Parameter Default Description window_region Region required

    The window region.

    region Region required

    The region to move inside the window.

    top bool False

    Get offset to top of window.

    Returns Type Description Offset

    An offset required to add to region to move it inside window_region.

    "},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
    def grow(self, margin):\n

    Grow a region by adding spacing.

    Parameters Parameter Default Description margin tuple[int, int, int, int] required

    Grow space by (<top>, <right>, <bottom>, <left>).

    Returns Type Description Region

    New region.

    "},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect method","text":"
    def inflect(self, x_axis=+1, y_axis=+1, margin=None):\n

    Inflect a region around one or both axis.

    The x_axis and y_axis parameters define which direction to move the region. A positive value will move the region right or down, a negative value will move the region left or up. A value of 0 will leave that axis unmodified.

    \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557    \u2502\n\u2551          \u2551\n\u2551   Self   \u2551    \u2502\n\u2551          \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d    \u2502\n\n\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u253c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\n\n                \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502          \u2502\n                \u2502    \u2502  Result  \u2502\n                     \u2502          \u2502\n                \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description x_axis int +1

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    y_axis int +1

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    margin Spacing | None None

    Additional margin.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
    def intersection(self, region):\n

    Get the overlapping portion of the two regions.

    Parameters Parameter Default Description region Region required

    A region that overlaps this region.

    Returns Type Description Region

    A new region that covers when the two regions overlap.

    "},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
    def overlaps(self, other):\n

    Check if another region overlaps this region.

    Parameters Parameter Default Description other Region required

    A Region.

    Returns Type Description bool

    True if other region shares any cells with this region.

    "},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
    def shrink(self, margin):\n

    Shrink a region by subtracting spacing.

    Parameters Parameter Default Description margin tuple[int, int, int, int] required

    Shrink space by (<top>, <right>, <bottom>, <left>).

    Returns Type Description Region

    The new, smaller region.

    "},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
    def split(self, cut_x, cut_y):\n

    Split a region in to 4 from given x and y offsets (cuts).

               cut_x \u2193\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502        \u2502 \u2502   \u2502\n        \u2502    0   \u2502 \u2502 1 \u2502\n        \u2502        \u2502 \u2502   \u2502\ncut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502    2   \u2502 \u2502 3 \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut_x int required

    Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

    cut_y int required

    Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

    Returns Type Description tuple[Region, Region, Region, Region]

    Four new regions which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
    def split_horizontal(self, cut):\n

    Split a region in to two, from a given y offset.

                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    0    \u2502\n            \u2502         \u2502\n    cut \u2192   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    1    \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut int required

    An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

    Returns Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
    def split_vertical(self, cut):\n

    Split a region in to two, from a given x offset.

             cut \u2193\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2510\n    \u2502    0   \u2502\u2502 1 \u2502\n    \u2502        \u2502\u2502   \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut int required

    An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

    Returns Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
    def translate(self, offset):\n

    Move the offset of the Region.

    Parameters Parameter Default Description offset tuple[int, int] required

    Offset to add to region.

    Returns Type Description Region

    A new region shifted by (x, y)

    "},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside method","text":"
    def translate_inside(self, container, x_axis=True, y_axis=True):\n

    Translate this region, so it fits within a container.

    This will ensure that there is as little overlap as possible. The top left of the returned region is guaranteed to be within the container.

    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502    container     \u2502         \u2502    container     \u2502\n\u2502                  \u2502         \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502                  \u2502   \u2500\u2500\u25b6   \u2502    \u2502    return   \u2502\n\u2502       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2510      \u2502    \u2502             \u2502\n\u2502       \u2502    self     \u2502      \u2502    \u2502             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524             \u2502      \u2514\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502             \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description container Region required

    A container region.

    x_axis bool True

    Allow translation of X axis.

    y_axis bool True

    Allow translation of Y axis.

    Returns Type Description Region

    A new region with same dimensions that fits with inside container.

    "},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
    def union(self, region):\n

    Get the smallest region that contains both regions.

    Parameters Parameter Default Description region Region required

    Another region.

    Returns Type Description Region

    An optimally sized region to cover both regions.

    "},{"location":"api/geometry/#textual.geometry.Size","title":"Size class","text":"

    Bases: NamedTuple

    The dimensions (width and height) of a rectangular region.

    Example
    >>> from textual.geometry import Size\n>>> size = Size(2, 3)\n>>> size\nSize(width=2, height=3)\n>>> size.area\n6\n>>> size + Size(10, 20)\nSize(width=12, height=23)\n
    "},{"location":"api/geometry/#textual.geometry.Size.area","title":"area property","text":"
    area: int\n

    The area occupied by a region of this size.

    "},{"location":"api/geometry/#textual.geometry.Size.height","title":"height instance-attribute class-attribute","text":"
    height: int = 0\n

    The height in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
    line_range: range\n

    A range object that covers values between 0 and height.

    "},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
    region: Region\n

    A region of the same size, at the origin.

    "},{"location":"api/geometry/#textual.geometry.Size.width","title":"width instance-attribute class-attribute","text":"
    width: int = 0\n

    The width in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains method","text":"
    def contains(self, x, y):\n

    Check if a point is in area defined by the size.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point method","text":"
    def contains_point(self, point):\n

    Check if a point is in the area defined by the size.

    Parameters Parameter Default Description point tuple[int, int] required

    A tuple of x and y coordinates.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing class","text":"

    Bases: NamedTuple

    The spacing around a renderable, such as padding and border.

    Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.

    \u250c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25b2\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2510\n               \u2502 top\n\u2502        \u250f\u2501\u2501\u2501\u2501\u2501\u25bc\u2501\u2501\u2501\u2501\u2501\u2501\u2513         \u2502\n \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2503            \u2503\u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n\u2502  left  \u2503            \u2503 right   \u2502\n         \u2503            \u2503\n\u2502        \u2517\u2501\u2501\u2501\u2501\u2501\u25b2\u2501\u2501\u2501\u2501\u2501\u2501\u251b         \u2502\n               \u2502 bottom\n\u2514 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25bc\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2518\n
    Example
    >>> from textual.geometry import Region, Spacing\n>>> region = Region(2, 3, 20, 10)\n>>> spacing = Spacing(1, 2, 3, 4)\n>>> region.grow(spacing)\nRegion(x=-2, y=2, width=26, height=14)\n>>> region.shrink(spacing)\nRegion(x=6, y=4, width=14, height=6)\n>>> spacing.css\n'1 2 3 4'\n
    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom","title":"bottom instance-attribute class-attribute","text":"
    bottom: int = 0\n

    Space from the bottom of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
    bottom_right: tuple[int, int]\n

    A pair of integers for the right, and bottom space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
    css: str\n

    A string containing the spacing in CSS format.

    For example: \"1\" or \"2 4\" or \"4 2 8 2\".

    "},{"location":"api/geometry/#textual.geometry.Spacing.height","title":"height property","text":"
    height: int\n

    Total space in the y axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left instance-attribute class-attribute","text":"
    left: int = 0\n

    Space from the left of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right instance-attribute class-attribute","text":"
    right: int = 0\n

    Space from the right of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top instance-attribute class-attribute","text":"
    top: int = 0\n

    Space from the top of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
    top_left: tuple[int, int]\n

    A pair of integers for the left, and top space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
    totals: tuple[int, int]\n

    A pair of integers for the total horizontal and vertical space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
    width: int\n

    Total space in the x axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
    def all(cls, amount):\n

    Construct a Spacing with a given amount of spacing on all edges.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to all edges.

    Returns Type Description Spacing

    Spacing(amount, amount, amount, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum method","text":"
    def grow_maximum(self, other):\n

    Grow spacing with a maximum.

    Parameters Parameter Default Description other Spacing required

    Spacing object.

    Returns Type Description Spacing

    New spacing where the values are maximum of the two values.

    "},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
    def horizontal(cls, amount):\n

    Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to horizontal edges.

    Returns Type Description Spacing

    Spacing(0, amount, 0, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
    def unpack(cls, pad):\n

    Unpack padding specified in CSS style.

    Parameters Parameter Default Description pad SpacingDimensions required

    An integer, or tuple of 1, 2, or 4 integers.

    Raises Type Description ValueError

    If pad is an invalid value.

    Returns Type Description Spacing

    New Spacing object.

    "},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
    def vertical(cls, amount):\n

    Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to vertical edges.

    Returns Type Description Spacing

    Spacing(amount, 0, amount, 0)

    "},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp function","text":"
    def clamp(value, minimum, maximum):\n

    Adjust a value so it is not less than a minimum and not greater than a maximum value.

    Parameters Parameter Default Description value T required

    A value.

    minimum T required

    Minimum value.

    maximum T required

    Maximum value.

    Returns Type Description T

    New value that is not less than the minimum or greater than the maximum.

    "},{"location":"api/lazy/","title":"Lazy","text":"

    Tools for lazy loading widgets.

    "},{"location":"api/lazy/#textual.lazy.Lazy","title":"Lazy class","text":"
    def __init__(self, widget):\n

    Bases: Widget

    Wraps a widget so that it is mounted lazily.

    Lazy widgets are mounted after the first refresh. This can be used to display some parts of the UI very quickly, followed by the lazy widgets. Technically, this won't make anything faster, but it reduces the time the user sees a blank screen and will make apps feel more responsive.

    Making a widget lazy is beneficial for widgets which start out invisible, such as tab panes.

    Note that since lazy widgets aren't mounted immediately (by definition), they will not appear in queries for a brief interval until they are mounted. Your code should take this in to account.

    Example
    def compose(self) -> ComposeResult:\n    yield Footer()\n    with ColorTabs(\"Theme Colors\", \"Named Colors\"):\n        yield Content(ThemeColorButtons(), ThemeColorsView(), id=\"theme\")\n        yield Lazy(NamedColorsView())\n
    Parameters Parameter Default Description widget Widget required

    A widget that should be mounted after a refresh.

    "},{"location":"api/logger/","title":"Logger","text":"

    A logger class that logs to the Textual console.

    "},{"location":"api/logger/#textual.Logger","title":"textual.Logger class","text":"
    def __init__(\n    self,\n    log_callable,\n    group=LogGroup.INFO,\n    verbosity=LogVerbosity.NORMAL,\n):\n

    A Textual logger.

    "},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
    debug: Logger\n

    Logs debug messages.

    "},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
    error: Logger\n

    Logs errors.

    "},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
    event: Logger\n

    Logs events.

    "},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
    info: Logger\n

    Logs information.

    "},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
    logging: Logger\n

    Logs from stdlib logging module.

    "},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
    system: Logger\n

    Logs system information.

    "},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
    verbose: Logger\n

    A verbose logger.

    "},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
    warning: Logger\n

    Logs warnings.

    "},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
    worker: Logger\n

    Logs worker information.

    "},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity method","text":"
    def verbosity(self, verbose):\n

    Get a new logger with selective verbosity.

    Parameters Parameter Default Description verbose bool required

    True to use HIGH verbosity, otherwise NORMAL.

    Returns Type Description Logger

    New logger.

    "},{"location":"api/logging/","title":"Logging","text":"

    A Textual Logging handler.

    If there is an active Textual app, then log messages will go via the app (and logged via textual console).

    If there is no active app, then log messages will go to stderr or stdout, depending on configuration.

    "},{"location":"api/logging/#textual.logging.TextualHandler","title":"TextualHandler class","text":"
    def __init__(self, stderr=True, stdout=False):\n

    Bases: Handler

    A Logging handler for Textual apps.

    Parameters Parameter Default Description stderr bool True

    Log to stderr when there is no active app.

    stdout bool False

    Log to stdout when there is not active app.

    "},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit method","text":"
    def emit(self, record):\n

    Invoked by logging.

    "},{"location":"api/map_geometry/","title":"Map geometry","text":"

    A data structure returned by screen.find_widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry","title":"textual._compositor.MapGeometry class","text":"

    Bases: NamedTuple

    Defines the absolute location of a Widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.clip","title":"clip instance-attribute","text":"
    clip: Region\n

    A region to clip the widget by (if a Widget is within a container).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.container_size","title":"container_size instance-attribute","text":"
    container_size: Size\n

    The container size (area not occupied by scrollbars).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
    dock_gutter: Spacing\n

    Space from the container reserved by docked widgets.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.order","title":"order instance-attribute","text":"
    order: tuple[tuple[int, int, int], ...]\n

    Tuple of tuples defining the painting order of the widget.

    Each successive triple represents painting order information with regards to ancestors in the DOM hierarchy and the last triple provides painting order information for this specific widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.region","title":"region instance-attribute","text":"
    region: Region\n

    The (screen) region occupied by the widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
    virtual_region: Region\n

    The region relative to the container (but not necessarily visible).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size: Size\n

    The virtual size (scrollable area) of a widget if it is a container.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.visible_region","title":"visible_region property","text":"
    visible_region: Region\n

    The Widget region after clipping.

    "},{"location":"api/message/","title":"Message","text":"

    The base class for all messages (including events).

    "},{"location":"api/message/#textual.message.Message","title":"Message class","text":"
    def __init__(self):\n

    Base class for a message.

    "},{"location":"api/message/#textual.message.Message.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute","text":"
    ALLOW_SELECTOR_MATCH: set[str] = set()\n

    Additional attributes that can be used with the on decorator.

    These attributes must be widgets.

    "},{"location":"api/message/#textual.message.Message.control","title":"control property","text":"
    control: Widget | None\n

    The widget associated with this message, or None by default.

    "},{"location":"api/message/#textual.message.Message.handler_name","title":"handler_name class-attribute","text":"
    handler_name: str\n

    Name of the default message handler.

    "},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
    is_forwarded: bool\n

    Has the message been forwarded?

    "},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default method","text":"
    def prevent_default(self, prevent=True):\n

    Suppress the default action(s). This will prevent handlers in any base classes from being called.

    Parameters Parameter Default Description prevent bool True

    True if the default action should be suppressed, or False if the default actions should be performed.

    "},{"location":"api/message/#textual.message.Message.stop","title":"stop method","text":"
    def stop(self, stop=True):\n

    Stop propagation of the message to parent.

    Parameters Parameter Default Description stop bool True

    The stop flag.

    "},{"location":"api/message_pump/","title":"Message pump","text":"

    A MessagePump is a base class for any object which processes messages, which includes Widget, Screen, and App.

    Tip

    Most of the method here are useful in general app development.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump","title":"MessagePump class","text":"
    def __init__(self, parent=None):\n

    Base class which supplies a message pump.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
    app: 'App[object]'\n

    Get the current app.

    Returns Type Description 'App[object]'

    The current app.

    Raises Type Description NoActiveAppError

    if no active app could be found for the current asyncio context

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.has_parent","title":"has_parent property","text":"
    has_parent: bool\n

    Does this object have a parent?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
    is_attached: bool\n

    Is the node attached to the app via the DOM?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
    is_parent_active: bool\n

    Is the parent active?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
    is_running: bool\n

    Is the message pump running (potentially processing messages)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
    log: Logger\n

    Get a logger for this object.

    Returns Type Description Logger

    A logger.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh method","text":"
    def call_after_refresh(self, callback, *args, **kwargs):\n

    Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable.

    Parameters Parameter Default Description callback Callback required

    A callable.

    Returns Type Description bool

    True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later method","text":"
    def call_later(self, callback, *args, **kwargs):\n

    Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable.

    Parameters Parameter Default Description callback Callback required

    Callable to call next.

    *args Any ()

    Positional arguments to pass to the callable.

    **kwargs Any {}

    Keyword arguments to pass to the callable.

    Returns Type Description bool

    True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next method","text":"
    def call_next(self, callback, *args, **kwargs):\n

    Schedule a callback to run immediately after processing the current message.

    Parameters Parameter Default Description callback Callback required

    Callable to run after current event.

    *args Any ()

    Positional arguments to pass to the callable.

    **kwargs Any {}

    Keyword arguments to pass to the callable.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle method","text":"
    def check_idle(self):\n

    Prompt the message pump to call idle if the queue is empty.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_message_enabled","title":"check_message_enabled method","text":"
    def check_message_enabled(self, message):\n

    Check if a given message is enabled (allowed to be sent).

    Parameters Parameter Default Description message Message required

    A message object.

    Returns Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages method","text":"
    def disable_messages(self, *messages):\n

    Disable message types from being processed.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.dispatch_key","title":"dispatch_key async","text":"
    def dispatch_key(self, event):\n

    Dispatch a key event to method.

    This method will call the method named 'key_' if it exists. Some keys have aliases. The first alias found will be invoked if it exists. If multiple handlers exist that match the key, an exception is raised. Parameters Parameter Default Description event events.Key required

    A key event.

    Returns Type Description bool

    True if key was handled, otherwise False.

    Raises Type Description DuplicateKeyHandlers

    When there's more than 1 handler that could handle this key.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages method","text":"
    def enable_messages(self, *messages):\n

    Enable processing of messages types.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
    def on_event(self, event):\n

    Called to process an event.

    Parameters Parameter Default Description event events.Event required

    An Event object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message method","text":"
    def post_message(self, message):\n

    Posts a message on to this widget's queue.

    Parameters Parameter Default Description message Message required

    A message (including Event).

    Returns Type Description bool

    True if the messages was processed, False if it wasn't.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent method","text":"
    def prevent(self, *message_types):\n

    A context manager to temporarily prevent the given message types from being posted.

    Example
    input = self.query_one(Input)\nwith self.prevent(Input.Changed):\n    input.value = \"foo\"\n
    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval","title":"set_interval method","text":"
    def set_interval(\n    self,\n    interval,\n    callback=None,\n    *,\n    name=None,\n    repeat=0,\n    pause=False\n):\n

    Call a function at periodic intervals.

    Parameters Parameter Default Description interval float required

    Time between calls.

    callback TimerCallback | None None

    Function to call.

    name str | None None

    Name of the timer object.

    repeat int 0

    Number of times to repeat the call or 0 for continuous.

    pause bool False

    Start the timer paused.

    Returns Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer method","text":"
    def set_timer(\n    self, delay, callback=None, *, name=None, pause=False\n):\n

    Make a function call after a delay.

    Parameters Parameter Default Description delay float required

    Time to wait before invoking callback.

    callback TimerCallback | None None

    Callback to call after time has expired.

    name str | None None

    Name of the timer (for debug).

    pause bool False

    Start timer paused.

    Returns Type Description Timer

    A timer object.

    "},{"location":"api/on/","title":"On","text":"

    Decorator to declare that the method is a message handler.

    The decorator accepts an optional CSS selector that will be matched against a widget exposed by a control property on the message.

    Example
    # Handle the press of buttons with ID \"#quit\".\n@on(Button.Pressed, \"#quit\")\ndef quit_button(self) -> None:\n    self.app.quit()\n

    Keyword arguments can be used to match additional selectors for attributes listed in ALLOW_SELECTOR_MATCH.

    Example
    # Handle the activation of the tab \"#home\" within the `TabbedContent` \"#tabs\".\n@on(TabbedContent.TabActivated, \"#tabs\", tab=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n
    Parameters Parameter Default Description message_type type[Message] required

    The message type (i.e. the class).

    selector str | None None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    **kwargs str {}

    Additional selectors for other attributes of the message.

    "},{"location":"api/pilot/","title":"Pilot","text":"

    The pilot object is used by App.run_test to programmatically operate an app.

    See the guide on how to test Textual apps.

    "},{"location":"api/pilot/#textual.pilot.OutOfBounds","title":"OutOfBounds class","text":"

    Bases: Exception

    Raised when the pilot mouse target is outside of the (visible) screen.

    "},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot class","text":"
    def __init__(self, app):\n

    Bases: Generic[ReturnType]

    Pilot object to drive an app.

    "},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
    app: App[ReturnType]\n
    "},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
    def click(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate clicking with the mouse at a specified position.

    The final position to be clicked is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Example

    The code below runs an app and clicks its only button right in the middle:

    async with SingleButtonApp().run_test() as pilot:\n    await pilot.click(Button, offset=(8, 1))\n

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the click offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to click on a specific widget. However, if the widget is currently hidden or obscured by another widget, the click may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset to click. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Click with the shift key held down.

    meta bool False

    Click with the meta key held down.

    control bool False

    Click with the control key held down.

    Raises Type Description OutOfBounds

    If the position to be clicked is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the click landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
    def exit(self, result):\n

    Exit the app with the given result.

    Parameters Parameter Default Description result ReturnType required

    The app result returned by run or run_async.

    "},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
    def hover(self, selector=None, offset=(0, 0)):\n

    Simulate hovering with the mouse cursor at a specified position.

    The final position to be hovered is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None | None None

    A selector to specify a widget that should be used as the reference for the hover offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to hover a specific widget. However, if the widget is currently hidden or obscured by another widget, the hover may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset to hover. The offset is relative to the selector provided or to the screen, if no selector is provided.

    Raises Type Description OutOfBounds

    If the position to be hovered is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the hover landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down","title":"mouse_down async","text":"
    def mouse_down(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate a MouseDown event at a specified position.

    The final position for the event is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the event offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to target a specific widget. However, if the widget is currently hidden or obscured by another widget, the event may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Simulate the event with the shift key held down.

    meta bool False

    Simulate the event with the meta key held down.

    control bool False

    Simulate the event with the control key held down.

    Raises Type Description OutOfBounds

    If the position for the event is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the event landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up","title":"mouse_up async","text":"
    def mouse_up(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate a MouseUp event at a specified position.

    The final position for the event is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the event offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to target a specific widget. However, if the widget is currently hidden or obscured by another widget, the event may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Simulate the event with the shift key held down.

    meta bool False

    Simulate the event with the meta key held down.

    control bool False

    Simulate the event with the control key held down.

    Raises Type Description OutOfBounds

    If the position for the event is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the event landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
    def pause(self, delay=None):\n

    Insert a pause.

    Parameters Parameter Default Description delay float | None None

    Seconds to pause, or None to wait for cpu idle.

    "},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
    def press(self, *keys):\n

    Simulate key-presses.

    Parameters Parameter Default Description *keys str ()

    Keys to press.

    "},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
    def wait_for_animation(self):\n

    Wait for any current animation to complete.

    "},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_scheduled_animations","title":"wait_for_scheduled_animations async","text":"
    def wait_for_scheduled_animations(self):\n

    Wait for any current and scheduled animations to complete.

    "},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout class","text":"

    Bases: Exception

    Exception raised if messages aren't being processed quickly enough.

    If this occurs, the most likely explanation is some kind of deadlock in the app code.

    "},{"location":"api/query/","title":"Query","text":"

    A DOMQuery is a set of DOM nodes returned by query.

    The set of nodes may be further refined with filter and exclude. Additional methods apply actions to all nodes in the query.

    Info

    If this sounds like JQuery, a (once) popular JS library, it is no coincidence.

    "},{"location":"api/query/#textual.css.query.ExpectType","title":"ExpectType module-attribute","text":"
    ExpectType = TypeVar('ExpectType')\n

    Type variable used to further restrict queries.

    "},{"location":"api/query/#textual.css.query.QueryType","title":"QueryType module-attribute","text":"
    QueryType = TypeVar('QueryType', bound='Widget')\n

    Type variable used to type generic queries.

    "},{"location":"api/query/#textual.css.query.DOMQuery","title":"DOMQuery class","text":"
    def __init__(\n    self, node, *, filter=None, exclude=None, parent=None\n):\n

    Bases: Generic[QueryType]

    Warning

    You won't need to construct this manually, as DOMQuery objects are returned by query.

    Parameters Parameter Default Description node DOMNode required

    A DOM node.

    filter str | None None

    Query to filter children in the node.

    exclude str | None None

    Query to exclude children in the node.

    parent DOMQuery | None None

    The parent query, if this is the result of filtering another query.

    Raises Type Description InvalidQueryFormat

    If the format of the query is invalid.

    "},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
    node: DOMNode\n

    The node being queried.

    "},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
    nodes: list[QueryType]\n

    Lazily evaluate nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class method","text":"
    def add_class(self, *class_names):\n

    Add the given class name(s) to nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.exclude","title":"exclude method","text":"
    def exclude(self, selector):\n

    Exclude nodes that match a given selector.

    Parameters Parameter Default Description selector str required

    A CSS selector.

    Returns Type Description DOMQuery[QueryType]

    New DOM query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter method","text":"
    def filter(self, selector):\n

    Filter this set by the given CSS selector.

    Parameters Parameter Default Description selector str required

    A CSS selector.

    Returns Type Description DOMQuery[QueryType]

    New DOM Query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first method","text":"
    def first(self, expect_type=None):\n

    Get the first matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If there are no matching nodes in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.last","title":"last method","text":"
    def last(self, expect_type=None):\n

    Get the last matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If there are no matching nodes in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one method","text":"
    def only_one(self, expect_type=None):\n

    Get the only matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    TooManyMatches

    If there is more than one matching node in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh method","text":"
    def refresh(self, *, repaint=True, layout=False):\n

    Refresh matched nodes.

    Parameters Parameter Default Description repaint bool True

    Repaint node(s).

    layout bool False

    Layout node(s).

    Returns Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove method","text":"
    def remove(self):\n

    Remove matched nodes from the DOM.

    Returns Type Description AwaitRemove

    An awaitable object that waits for the widgets to be removed.

    "},{"location":"api/query/#textual.css.query.DOMQuery.remove_class","title":"remove_class method","text":"
    def remove_class(self, *class_names):\n

    Remove the given class names from the nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results method","text":"
    def results(self, filter_type=None):\n

    Get query results, optionally filtered by a given type.

    Parameters Parameter Default Description filter_type type[ExpectType] | None None

    A Widget class to filter results, or None for no filter.

    Yields:

    Type Description QueryType | ExpectType

    Iterator[Widget | ExpectType]: An iterator of Widget instances.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class method","text":"
    def set_class(self, add, *class_names):\n

    Set the given class name(s) according to a condition.

    Parameters Parameter Default Description add bool required

    Add the classes if True, otherwise remove them.

    Returns Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes method","text":"
    def set_classes(self, classes):\n

    Set the classes on nodes to exactly the given set.

    Parameters Parameter Default Description classes str | Iterable[str] required

    A string of space separated classes, or an iterable of class names.

    Returns Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles method","text":"
    def set_styles(self, css=None, **update_styles):\n

    Set styles on matched nodes.

    Parameters Parameter Default Description css str | None None

    CSS declarations to parser, or None.

    "},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class method","text":"
    def toggle_class(self, *class_names):\n

    Toggle the given class names from matched nodes.

    "},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat class","text":"

    Bases: QueryError

    Query did not parse correctly.

    "},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches class","text":"

    Bases: QueryError

    No nodes matched the query.

    "},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError class","text":"

    Bases: Exception

    Base class for a query related error.

    "},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches class","text":"

    Bases: QueryError

    Too many nodes matched the query.

    "},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType class","text":"

    Bases: QueryError

    Query result was not of the correct type.

    "},{"location":"api/reactive/","title":"Reactive","text":"

    The Reactive class implements reactivity.

    "},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive class","text":"
    def __init__(\n    self,\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=False,\n    always_update=False,\n    compute=True\n):\n

    Bases: Generic[ReactiveType]

    Reactive descriptor.

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    layout bool False

    Perform a layout on change.

    repaint bool True

    Perform a repaint on change.

    init bool False

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    compute bool True

    Run compute methods when attribute is changed.

    "},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError class","text":"

    Bases: Exception

    Raised when an attribute has public and private compute methods.

    "},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive class","text":"
    def __init__(\n    self,\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=True,\n    always_update=False\n):\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute.

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    layout bool False

    Perform a layout on change.

    repaint bool True

    Perform a repaint on change.

    init bool True

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    "},{"location":"api/reactive/#textual.reactive.var","title":"var class","text":"
    def __init__(self, default, init=True, always_update=False):\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute (with no auto-refresh).

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    init bool True

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    "},{"location":"api/renderables/","title":"Renderables","text":"

    A collection of Rich renderables which may be returned from a widget's render() method.

    "},{"location":"api/renderables/#textual.renderables.bar.Bar","title":"Bar class","text":"
    def __init__(\n    self,\n    highlight_range=(0, 0),\n    highlight_style=\"magenta\",\n    background_style=\"grey37\",\n    clickable_ranges=None,\n    width=None,\n):\n

    Thin horizontal bar with a portion highlighted.

    Parameters Parameter Default Description highlight_range tuple[float, float] (0, 0)

    The range to highlight.

    highlight_style StyleType 'magenta'

    The style of the highlighted range of the bar.

    background_style StyleType 'grey37'

    The style of the non-highlighted range(s) of the bar.

    width int | None None

    The width of the bar, or None to fill available width.

    "},{"location":"api/renderables/#textual.renderables.blank.Blank","title":"Blank class","text":"
    def __init__(self, color='transparent'):\n

    Draw solid background color.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits","title":"Digits class","text":"
    def __init__(self, text, style=''):\n

    Renders a 3X3 unicode 'font' for numerical values.

    Parameters Parameter Default Description text str required

    Text to display.

    style StyleType ''

    Style to apply to the digits.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width","title":"get_width classmethod","text":"
    def get_width(cls, text):\n

    Calculate the width without rendering.

    Parameters Parameter Default Description text str required

    Text which may be displayed in the Digits widget.

    Returns Type Description int

    width of the text (in cells).

    "},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient","title":"LinearGradient class","text":"
    def __init__(self, angle, stops):\n

    Render a linear gradient with a rotation.

    Parameters Parameter Default Description angle float required

    Angle of rotation in degrees.

    stops Sequence[tuple[float, Color | str]] required

    List of stop consisting of pairs of offset (between 0 and 1) and color.

    "},{"location":"api/renderables/#textual.renderables.gradient.VerticalGradient","title":"VerticalGradient class","text":"
    def __init__(self, color1, color2):\n

    Draw a vertical gradient.

    "},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline","title":"Sparkline class","text":"
    def __init__(\n    self,\n    data,\n    *,\n    width,\n    min_color=Color.from_rgb(0, 255, 0),\n    max_color=Color.from_rgb(255, 0, 0),\n    summary_function=max\n):\n

    Bases: Generic[T]

    A sparkline representing a series of data.

    Parameters Parameter Default Description data Sequence[T] required

    The sequence of data to render.

    width int | None required

    The width of the sparkline/the number of buckets to partition the data into.

    min_color Color Color.from_rgb(0, 255, 0)

    The color of values equal to the min value in data.

    max_color Color Color.from_rgb(255, 0, 0)

    The color of values equal to the max value in data.

    summary_function SummaryFunction[T] max

    Function that will be applied to each bucket.

    "},{"location":"api/screen/","title":"Screen","text":"

    The Screen class is a special widget which represents the content in the terminal. See Screens for details.

    "},{"location":"api/screen/#textual.screen.ScreenResultCallbackType","title":"ScreenResultCallbackType module-attribute","text":"
    ScreenResultCallbackType = Union[\n    Callable[[ScreenResultType], None],\n    Callable[[ScreenResultType], Awaitable[None]],\n]\n

    Type of a screen result callback function.

    "},{"location":"api/screen/#textual.screen.ScreenResultType","title":"ScreenResultType module-attribute","text":"
    ScreenResultType = TypeVar('ScreenResultType')\n

    The result type of a screen.

    "},{"location":"api/screen/#textual.screen.ModalScreen","title":"ModalScreen class","text":"
    def __init__(self, name=None, id=None, classes=None):\n

    Bases: Screen[ScreenResultType]

    A screen with bindings that take precedence over the App's key bindings.

    The default styling of a modal screen will dim the screen underneath.

    "},{"location":"api/screen/#textual.screen.ResultCallback","title":"ResultCallback class","text":"
    def __init__(self, requester, callback, future=None):\n

    Bases: Generic[ScreenResultType]

    Holds the details of a callback.

    Parameters Parameter Default Description requester MessagePump required

    The object making a request for the callback.

    callback ScreenResultCallbackType[ScreenResultType] | None required

    The callback function.

    future asyncio.Future[ScreenResultType] | None None

    A Future to hold the result.

    "},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
    callback: ScreenResultCallbackType | None = callback\n

    The callback function.

    "},{"location":"api/screen/#textual.screen.ResultCallback.future","title":"future instance-attribute","text":"
    future = future\n

    A future for the result

    "},{"location":"api/screen/#textual.screen.ResultCallback.requester","title":"requester instance-attribute","text":"
    requester = requester\n

    The object in the DOM that requested the callback.

    "},{"location":"api/screen/#textual.screen.Screen","title":"Screen class","text":"
    def __init__(self, name=None, id=None, classes=None):\n

    Bases: Generic[ScreenResultType], Widget

    The base class for screens.

    Parameters Parameter Default Description name str | None None

    The name of the screen.

    id str | None None

    The ID of the screen in the DOM.

    classes str | None None

    The CSS classes for the screen.

    "},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS: str | None = None\n

    A selector to determine what to focus automatically when the screen is activated.

    The widget focused is the first that matches the given CSS selector. Set to None to inherit the value from the screen's app. Set to \"\" to disable auto focus.

    "},{"location":"api/screen/#textual.screen.Screen.COMMANDS","title":"COMMANDS class-attribute","text":"
    COMMANDS: set[\n    type[Provider] | Callable[[], type[Provider]]\n] = set()\n

    Command providers used by the command palette, associated with the screen.

    Should be a set of command.Provider classes.

    "},{"location":"api/screen/#textual.screen.Screen.CSS","title":"CSS class-attribute","text":"
    CSS: str = ''\n

    Inline CSS, useful for quick scripts. Rules here take priority over CSS_PATH.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.CSS_PATH","title":"CSS_PATH class-attribute","text":"
    CSS_PATH: CSSPathType | None = None\n

    File paths to load CSS from.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
    SUB_TITLE: str | None = None\n

    A class variable to set the default sub-title for the screen.

    This overrides the app sub-title. To update the sub-title while the screen is running, you can set the sub_title attribute.

    "},{"location":"api/screen/#textual.screen.Screen.TITLE","title":"TITLE class-attribute","text":"
    TITLE: str | None = None\n

    A class variable to set the default title for the screen.

    This overrides the app title. To update the title while the screen is running, you can set the title attribute.

    "},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
    focus_chain: list[Widget]\n

    A list of widgets that may receive focus, in focus order.

    "},{"location":"api/screen/#textual.screen.Screen.focused","title":"focused instance-attribute class-attribute","text":"
    focused: Reactive[Widget | None] = Reactive(None)\n

    The focused widget or None for no focus.

    "},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
    is_current: bool\n

    Is the screen current (i.e. visible to user)?

    "},{"location":"api/screen/#textual.screen.Screen.is_modal","title":"is_modal property","text":"
    is_modal: bool\n

    Is the screen modal?

    "},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
    layers: tuple[str, ...]\n

    Layers from parent.

    Returns Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates instance-attribute class-attribute","text":"
    stack_updates: Reactive[int] = Reactive(0, repaint=False)\n

    An integer that updates when the screen is resumed.

    "},{"location":"api/screen/#textual.screen.Screen.sub_title","title":"sub_title instance-attribute class-attribute","text":"
    sub_title: Reactive[str | None] = self.SUB_TITLE\n

    Screen sub-title to override the app sub-title.

    "},{"location":"api/screen/#textual.screen.Screen.title","title":"title instance-attribute class-attribute","text":"
    title: Reactive[str | None] = self.TITLE\n

    Screen title to override the app title.

    "},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss method","text":"
    def action_dismiss(self, result=_NoResult):\n

    A wrapper around dismiss that can be called as an action.

    Parameters Parameter Default Description result ScreenResultType | Type[_NoResult] _NoResult

    The optional result to be passed to the result callback.

    "},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view method","text":"
    def can_view(self, widget):\n

    Check if a given widget is in the current view (scrollable area).

    Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

    Parameters Parameter Default Description widget Widget required

    A widget that is a descendant of self.

    Returns Type Description bool

    True if the entire widget is in view, False if it is partially visible or not in view.

    "},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss method","text":"
    def dismiss(self, result=_NoResult):\n

    Dismiss the screen, optionally with a result.

    If result is provided and a callback was set when the screen was pushed, then the callback will be invoked with result.

    Parameters Parameter Default Description result ScreenResultType | Type[_NoResult] _NoResult

    The optional result to be passed to the result callback.

    Raises Type Description ScreenStackError

    If trying to dismiss a screen that is not at the top of the stack.

    "},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget method","text":"
    def find_widget(self, widget):\n

    Get the screen region of a Widget.

    Parameters Parameter Default Description widget Widget required

    A Widget within the composition.

    Returns Type Description MapGeometry

    Region relative to screen.

    Raises Type Description NoWidget

    If the widget could not be found in this screen.

    "},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next method","text":"
    def focus_next(self, selector='*'):\n

    Focus the next widget, optionally filtered by a CSS selector.

    If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

    Parameters Parameter Default Description selector str | type[QueryType] '*'

    CSS selector to filter what nodes can be focused.

    Returns Type Description Widget | None

    Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

    "},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous method","text":"
    def focus_previous(self, selector='*'):\n

    Focus the previous widget, optionally filtered by a CSS selector.

    If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

    Parameters Parameter Default Description selector str | type[QueryType] '*'

    CSS selector to filter what nodes can be focused.

    Returns Type Description Widget | None

    Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

    "},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset method","text":"
    def get_offset(self, widget):\n

    Get the absolute offset of a given Widget.

    Parameters Parameter Default Description widget Widget required

    A widget

    Returns Type Description Offset

    The widget's offset relative to the top left of the terminal.

    "},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at method","text":"
    def get_style_at(self, x, y):\n

    Get the style under a given coordinate.

    Parameters Parameter Default Description x int required

    X Coordinate.

    y int required

    Y Coordinate.

    Returns Type Description Style

    Rich Style object.

    "},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at method","text":"
    def get_widget_at(self, x, y):\n

    Get the widget at a given coordinate.

    Parameters Parameter Default Description x int required

    X Coordinate.

    y int required

    Y Coordinate.

    Returns Type Description tuple[Widget, Region]

    Widget and screen region.

    "},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at method","text":"
    def get_widgets_at(self, x, y):\n

    Get all widgets under a given coordinate.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description Iterable[tuple[Widget, Region]]

    Sequence of (WIDGET, REGION) tuples.

    "},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus method","text":"
    def set_focus(self, widget, scroll_visible=True):\n

    Focus (or un-focus) a widget. A focused widget will receive key events first.

    Parameters Parameter Default Description widget Widget | None required

    Widget to focus, or None to un-focus.

    scroll_visible bool True

    Scroll widget in to view.

    "},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title method","text":"
    def validate_sub_title(self, sub_title):\n

    Ensure the sub-title is a string or None.

    "},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title method","text":"
    def validate_title(self, title):\n

    Ensure the title is a string or None.

    "},{"location":"api/scroll_view/","title":"Scroll view","text":"

    ScrollView is a base class for line api widgets.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView class","text":"

    Bases: ScrollableContainer

    A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable: bool\n

    Always scrollable.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines method","text":"
    def refresh_lines(self, y_start, line_count=1):\n

    Refresh one or more lines.

    Parameters Parameter Default Description y_start int required

    First line to refresh.

    line_count int 1

    Total number of lines to refresh.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to method","text":"
    def scroll_to(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters Parameter Default Description x float | None None

    X coordinate (column) to scroll to, or None for no change.

    y float | None None

    Y coordinate (row) to scroll to, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/scrollbar/","title":"Scrollbar","text":"

    Implements the scrollbar-related widgets for internal use.

    You will not need to use the widgets defined in this module.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar class","text":"
    def __init__(self, vertical=True, name=None, *, thickness=1):\n

    Bases: Widget

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
    renderer: Type[ScrollBarRender] = ScrollBarRender\n

    The class used for rendering scrollbars. This can be overridden and set to a ScrollBarRender-derived class in order to delegate all scrollbar rendering to that class. E.g.:

    class MyScrollBarRender(ScrollBarRender): ...\n\napp = MyApp()\nScrollBar.renderer = MyScrollBarRender\napp.run()\n

    Because this variable is accessed through specific instances (rather than through the class ScrollBar itself) it is also possible to set this on specific scrollbar instance to change only that instance:

    my_widget.horizontal_scrollbar.renderer = MyScrollBarRender\n
    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_grab","title":"action_grab method","text":"
    def action_grab(self):\n

    Begin capturing the mouse cursor.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down method","text":"
    def action_scroll_down(self):\n

    Scroll vertical scrollbars down, horizontal scrollbars right.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up method","text":"
    def action_scroll_up(self):\n

    Scroll vertical scrollbars up, horizontal scrollbars left.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner class","text":"
    def __init__(self, name=None):\n

    Bases: Widget

    Widget which fills the gap between horizontal and vertical scrollbars, should they both be present.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown class","text":"

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft class","text":"

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage class","text":"

    Bases: Message

    Base class for all scrollbar messages.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight class","text":"

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo class","text":"
    def __init__(self, x=None, y=None, animate=True):\n

    Bases: ScrollMessage

    Message sent when click and dragging handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp class","text":"

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/strip/","title":"Strip","text":"

    A Strip contains the result of rendering a widget. See line API for how to use Strips.

    "},{"location":"api/strip/#textual.strip.Strip","title":"Strip class","text":"
    def __init__(self, segments, cell_length=None):\n

    Represents a 'strip' (horizontal line) of a Textual Widget.

    A Strip is like an immutable list of Segments. The immutability allows for effective caching.

    Parameters Parameter Default Description segments Iterable[Segment] required

    An iterable of segments.

    cell_length int | None None

    The cell length if known, or None to calculate on demand.

    "},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
    cell_length: int\n

    Get the number of cells required to render this object.

    "},{"location":"api/strip/#textual.strip.Strip.link_ids","title":"link_ids property","text":"
    link_ids: set[str]\n

    A set of the link ids in this Strip.

    "},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
    text: str\n

    Segment text.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length method","text":"
    def adjust_cell_length(self, cell_length, style=None):\n

    Adjust the cell length, possibly truncating or extending.

    Parameters Parameter Default Description cell_length int required

    New desired cell length.

    style Style | None None

    Style when extending, or None.

    Returns Type Description Strip

    A new strip with the supplied cell length.

    "},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter method","text":"
    def apply_filter(self, filter, background):\n

    Apply a filter to all segments in the strip.

    Parameters Parameter Default Description filter LineFilter required

    A line filter object.

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style method","text":"
    def apply_style(self, style):\n

    Apply a style to the Strip.

    Parameters Parameter Default Description style Style required

    A Rich style.

    Returns Type Description Strip

    A new strip.

    "},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
    def blank(cls, cell_length, style=None):\n

    Create a blank strip.

    Parameters Parameter Default Description cell_length int required

    Desired cell length.

    style StyleType | None None

    Style of blank.

    Returns Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop method","text":"
    def crop(self, start, end=None):\n

    Crop a strip between two cell positions.

    Parameters Parameter Default Description start int required

    The start cell position (inclusive).

    end int | None None

    The end cell position (exclusive).

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend method","text":"
    def crop_extend(self, start, end, style):\n

    Crop between two points, extending the length if required.

    Parameters Parameter Default Description start int required

    Start offset of crop.

    end int required

    End offset of crop.

    style Style | None required

    Style of additional padding.

    Returns Type Description Strip

    New cropped Strip.

    "},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide method","text":"
    def divide(self, cuts):\n

    Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

    Parameters Parameter Default Description cuts Iterable[int] required

    An iterable of cell positions as ints.

    Returns Type Description Sequence[Strip]

    A new list of strips.

    "},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length method","text":"
    def extend_cell_length(self, cell_length, style=None):\n

    Extend the cell length if it is less than the given value.

    Parameters Parameter Default Description cell_length int required

    Required minimum cell length.

    style Style | None None

    Style for padding if the cell length is extended.

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
    def from_lines(cls, lines, cell_length=None):\n

    Convert lines (lists of segments) to a list of Strips.

    Parameters Parameter Default Description lines list[list[Segment]] required

    List of lines, where a line is a list of segments.

    cell_length int | None None

    Cell length of lines (must be same) or None if not known.

    Returns Type Description list[Strip]

    List of strips.

    "},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position method","text":"
    def index_to_cell_position(self, index):\n

    Given a character index, return the cell position of that character. This is the sum of the cell lengths of all the characters before the character at index.

    Parameters Parameter Default Description index int required

    The index to convert.

    Returns Type Description int

    The cell position of the character at index.

    "},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
    def join(cls, strips):\n

    Join a number of strips in to one.

    Parameters Parameter Default Description strips Iterable[Strip | None] required

    An iterable of Strips.

    Returns Type Description Strip

    A new combined strip.

    "},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify method","text":"
    def simplify(self):\n

    Simplify the segments (join segments with same style)

    Returns Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.style_links","title":"style_links method","text":"
    def style_links(self, link_id, link_style):\n

    Apply a style to Segments with the given link_id.

    Parameters Parameter Default Description link_id str required

    A link id.

    link_style Style required

    Style to apply.

    Returns Type Description Strip

    New strip (or same Strip if no changes).

    "},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable class","text":"
    def __init__(self, strips, width=None):\n

    A renderable which renders a list of strips in to lines.

    "},{"location":"api/strip/#textual.strip.get_line_length","title":"get_line_length function","text":"
    def get_line_length(segments):\n

    Get the line length (total length of all segments).

    Parameters Parameter Default Description segments Iterable[Segment] required

    Iterable of segments.

    Returns Type Description int

    Length of line in cells.

    "},{"location":"api/suggester/","title":"Suggester","text":"

    The Suggester class is used by the Input widget.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList class","text":"
    def __init__(self, suggestions, *, case_sensitive=True):\n

    Bases: Suggester

    Give completion suggestions based on a fixed list of options.

    Example
    countries = [\"England\", \"Scotland\", \"Portugal\", \"Spain\", \"France\"]\n\nclass MyApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield Input(suggester=SuggestFromList(countries, case_sensitive=False))\n

    If the user types P inside the input widget, a completion suggestion for \"Portugal\" appears.

    Parameters Parameter Default Description suggestions Iterable[str] required

    Valid suggestions sorted by decreasing priority.

    case_sensitive bool True

    Whether suggestions are computed in a case sensitive manner or not. The values provided in the argument suggestions represent the canonical representation of the completions and they will be suggested with that same casing.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
    def get_suggestion(self, value):\n

    Gets a completion from the given possibilities.

    Parameters Parameter Default Description value str required

    The current value.

    Returns Type Description str | None

    A valid completion suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester class","text":"
    def __init__(self, *, use_cache=True, case_sensitive=False):\n

    Bases: ABC

    Defines how widgets generate completion suggestions.

    To define a custom suggester, subclass Suggester and implement the async method get_suggestion. See SuggestFromList for an example.

    Parameters Parameter Default Description use_cache bool True

    Whether to cache suggestion results.

    case_sensitive bool False

    Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

    "},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
    cache: LRUCache[str, str | None] | None = (\n    LRUCache(1024) if use_cache else None\n)\n

    Suggestion cache, if used.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion abstractmethod async","text":"
    def get_suggestion(self, value):\n

    Try to get a completion suggestion for the given input value.

    Custom suggesters should implement this method.

    Note

    The value argument will be casefolded if self.case_sensitive is False.

    Note

    If your implementation is not deterministic, you may need to disable caching.

    Parameters Parameter Default Description value str required

    The current value of the requester widget.

    Returns Type Description str | None

    A valid suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady class","text":"

    Bases: Message

    Sent when a completion suggestion is ready.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
    suggestion: str\n

    The string suggestion.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
    value: str\n

    The value to which the suggestion is for.

    "},{"location":"api/system_commands_source/","title":"System commands source","text":"

    A command palette command provider for Textual system commands.

    This is a simple command provider that makes the most obvious application actions available via the command palette.

    "},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands","title":"SystemCommands class","text":"

    Bases: Provider

    A source of command palette commands that run app-wide tasks.

    Used by default in App.COMMANDS.

    "},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands.search","title":"search async","text":"
    def search(self, query):\n

    Handle a request to search for system commands that match the query.

    Parameters Parameter Default Description query str required

    The user input to be matched.

    Yields:

    Type Description Hits

    Command hits for use in the command palette.

    "},{"location":"api/timer/","title":"Timer","text":"

    Timer objects are created by set_interval or set_timer.

    "},{"location":"api/timer/#textual.timer.TimerCallback","title":"TimerCallback module-attribute","text":"
    TimerCallback = Union[\n    Callable[[], Awaitable[Any]], Callable[[], Any]\n]\n

    Type of valid callbacks to be used with timers.

    "},{"location":"api/timer/#textual.timer.Timer","title":"Timer class","text":"
    def __init__(\n    self,\n    event_target,\n    interval,\n    *,\n    name=None,\n    callback=None,\n    repeat=None,\n    skip=True,\n    pause=False\n):\n

    A class to send timer-based events.

    Parameters Parameter Default Description event_target MessageTarget required

    The object which will receive the timer events.

    interval float required

    The time between timer events, in seconds.

    name str | None None

    A name to assign the event (for debugging).

    callback TimerCallback | None None

    A optional callback to invoke when the event is handled.

    repeat int | None None

    The number of times to repeat the timer, or None to repeat forever.

    skip bool True

    Enable skipping of scheduled events that couldn't be sent in time.

    pause bool False

    Start the timer paused.

    "},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause method","text":"
    def pause(self):\n

    Pause the timer.

    A paused timer will not send events until it is resumed.

    "},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset method","text":"
    def reset(self):\n

    Reset the timer, so it starts from the beginning.

    "},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume method","text":"
    def resume(self):\n

    Resume a paused timer.

    "},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop method","text":"
    def stop(self):\n

    Stop the timer.

    "},{"location":"api/types/","title":"Types","text":"

    Export some objects that are used by Textual and that help document other features.

    "},{"location":"api/types/#textual.types.ActionParseResult","title":"ActionParseResult module-attribute","text":"
    ActionParseResult: TypeAlias = \"tuple[str, tuple[Any, ...]]\"\n

    An action is its name and the arbitrary tuple of its arguments.

    "},{"location":"api/types/#textual.types.CSSPathType","title":"CSSPathType module-attribute","text":"
    CSSPathType: TypeAlias = Union[\n    str, PurePath, List[Union[str, PurePath]]\n]\n

    Valid ways of specifying paths to CSS files.

    "},{"location":"api/types/#textual.types.CallbackType","title":"CallbackType module-attribute","text":"
    CallbackType = Union[\n    Callable[[], Awaitable[None]], Callable[[], None]\n]\n

    Type used for arbitrary callables used in callbacks.

    "},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
    EasingFunction = Callable[[float], float]\n

    Signature for a function that parametrises animation speed.

    An easing function must map the interval [0, 1] into the interval [0, 1].

    "},{"location":"api/types/#textual.types.IgnoreReturnCallbackType","title":"IgnoreReturnCallbackType module-attribute","text":"
    IgnoreReturnCallbackType = Union[\n    Callable[[], Awaitable[Any]], Callable[[], Any]\n]\n

    A callback which ignores the return type.

    "},{"location":"api/types/#textual.types.InputValidationOn","title":"InputValidationOn module-attribute","text":"
    InputValidationOn = Literal['blur', 'changed', 'submitted']\n

    Possible messages that trigger input validation.

    "},{"location":"api/types/#textual.types.NewOptionListContent","title":"NewOptionListContent module-attribute","text":"
    NewOptionListContent: TypeAlias = (\n    \"OptionListContent | None | RenderableType\"\n)\n

    The type of a new item of option list content to be added to an option list.

    This type represents all of the types that will be accepted when adding new content to the option list. This is a superset of OptionListContent.

    "},{"location":"api/types/#textual.types.OptionListContent","title":"OptionListContent module-attribute","text":"
    OptionListContent: TypeAlias = 'Option | Separator'\n

    The type of an item of content in the option list.

    This type represents all of the types that will be found in the list of content of the option list after it has been processed for addition.

    "},{"location":"api/types/#textual.types.PlaceholderVariant","title":"PlaceholderVariant module-attribute","text":"
    PlaceholderVariant = Literal['default', 'size', 'text']\n

    The different variants of placeholder.

    "},{"location":"api/types/#textual.types.SelectType","title":"SelectType module-attribute","text":"
    SelectType = TypeVar('SelectType')\n

    The type used for data in the Select.

    "},{"location":"api/types/#textual.types.WatchCallbackType","title":"WatchCallbackType module-attribute","text":"
    WatchCallbackType = Union[\n    Callable[[], Awaitable[None]],\n    Callable[[Any], Awaitable[None]],\n    Callable[[Any, Any], Awaitable[None]],\n    Callable[[], None],\n    Callable[[Any], None],\n    Callable[[Any, Any], None],\n]\n

    Type used for callbacks passed to the watch method of widgets.

    "},{"location":"api/types/#textual.types.Animatable","title":"Animatable class","text":"

    Bases: Protocol

    Protocol for objects that can have their intrinsic values animated.

    For example, the transition between two colors can be animated because the class Color satisfies this protocol.

    "},{"location":"api/types/#textual.types.CSSPathError","title":"CSSPathError class","text":"

    Bases: Exception

    Raised when supplied CSS path(s) are invalid.

    "},{"location":"api/types/#textual.types.DirEntry","title":"DirEntry class","text":"

    Attaches directory information to a DirectoryTree node.

    "},{"location":"api/types/#textual.widgets._directory_tree.DirEntry.loaded","title":"loaded instance-attribute class-attribute","text":"
    loaded: bool = False\n

    Has this been loaded?

    "},{"location":"api/types/#textual.widgets._directory_tree.DirEntry.path","title":"path instance-attribute","text":"
    path: Path\n

    The path of the directory entry.

    "},{"location":"api/types/#textual.types.DuplicateID","title":"DuplicateID class","text":"

    Bases: Exception

    Raised if a duplicate ID is used when adding options to an option list.

    "},{"location":"api/types/#textual.types.MessageTarget","title":"MessageTarget class","text":"

    Bases: Protocol

    Protocol that must be followed by objects that can receive messages.

    "},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError class","text":"

    Bases: RuntimeError

    Runtime error raised if we try to retrieve the active app when there is none.

    "},{"location":"api/types/#textual.types.NoSelection","title":"NoSelection class","text":"

    Used by the Select widget to flag the unselected state. See Select.BLANK.

    "},{"location":"api/types/#textual.types.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

    Bases: Exception

    Raised when a request has been made for an option that doesn't exist.

    "},{"location":"api/types/#textual.types.RenderStyles","title":"RenderStyles class","text":"
    def __init__(self, node, base, inline_styles):\n

    Bases: StylesBase

    Presents a combined view of two Styles object: a base Styles and inline Styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.base","title":"base property","text":"
    base: Styles\n

    Quick access to base (css) style.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.css","title":"css property","text":"
    css: str\n

    Get the CSS for the combined styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.gutter","title":"gutter property","text":"
    gutter: Spacing\n

    Get space around widget.

    Returns Type Description Spacing

    Space around widget content.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.inline","title":"inline property","text":"
    inline: Styles\n

    Quick access to the inline styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.rich_style","title":"rich_style property","text":"
    rich_style: Style\n

    Get a Rich style for this Styles object.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value str | float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation. Defaults to value if not set.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.clear_rule","title":"clear_rule method","text":"
    def clear_rule(self, rule_name):\n

    Clear a rule (from inline).

    "},{"location":"api/types/#textual.css.styles.RenderStyles.get_rules","title":"get_rules method","text":"
    def get_rules(self):\n

    Get rules as a dictionary

    "},{"location":"api/types/#textual.css.styles.RenderStyles.has_rule","title":"has_rule method","text":"
    def has_rule(self, rule):\n

    Check if a rule has been set.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.merge","title":"merge method","text":"
    def merge(self, other):\n

    Merge values from another Styles.

    Parameters Parameter Default Description other StylesBase required

    A Styles object.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.reset","title":"reset method","text":"
    def reset(self):\n

    Reset the rules to initial state.

    "},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter class","text":"

    Helper type for a parameter that isn't specified in a method call.

    "},{"location":"api/validation/","title":"Validation","text":"

    Framework for validating string values

    "},{"location":"api/validation/#textual.validation.Failure","title":"Failure class","text":"

    Information about a validation failure.

    "},{"location":"api/validation/#textual.validation.Failure.description","title":"description instance-attribute class-attribute","text":"
    description: str | None = None\n

    An optional override for describing this failure. Takes precedence over any messages set in the Validator.

    "},{"location":"api/validation/#textual.validation.Failure.validator","title":"validator instance-attribute","text":"
    validator: Validator\n

    The Validator which produced the failure.

    "},{"location":"api/validation/#textual.validation.Failure.value","title":"value instance-attribute class-attribute","text":"
    value: str | None = None\n

    The value which resulted in validation failing.

    "},{"location":"api/validation/#textual.validation.Function","title":"Function class","text":"
    def __init__(self, function, failure_description=None):\n

    Bases: Validator

    A flexible validator which allows you to provide custom validation logic.

    "},{"location":"api/validation/#textual.validation.Function.function","title":"function instance-attribute","text":"
    function = function\n

    Function which takes the value to validate and returns True if valid, and False otherwise.

    "},{"location":"api/validation/#textual.validation.Function.ReturnedFalse","title":"ReturnedFalse class","text":"

    Bases: Failure

    Indicates validation failed because the supplied function returned False.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Function.validate","title":"validate method","text":"
    def validate(self, value):\n

    Validate that the supplied function returns True.

    Parameters Parameter Default Description value str required

    The value to pass into the supplied function.

    Returns Type Description ValidationResult

    A ValidationResult indicating success if the function returned True, and failure if the function return False.

    "},{"location":"api/validation/#textual.validation.Integer","title":"Integer class","text":"

    Bases: Number

    Validator which ensures the value is an integer which falls within a range.

    "},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger class","text":"

    Bases: Failure

    Indicates a failure due to the value not being a valid integer.

    "},{"location":"api/validation/#textual.validation.Integer.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value is an integer, optionally within a range.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Length","title":"Length class","text":"
    def __init__(\n    self,\n    minimum=None,\n    maximum=None,\n    failure_description=None,\n):\n

    Bases: Validator

    Validate that a string is within a range (inclusive).

    "},{"location":"api/validation/#textual.validation.Length.maximum","title":"maximum instance-attribute","text":"
    maximum = maximum\n

    The inclusive maximum length of the value, or None if unbounded.

    "},{"location":"api/validation/#textual.validation.Length.minimum","title":"minimum instance-attribute","text":"
    minimum = minimum\n

    The inclusive minimum length of the value, or None if unbounded.

    "},{"location":"api/validation/#textual.validation.Length.Incorrect","title":"Incorrect class","text":"

    Bases: Failure

    Indicates a failure due to the length of the value being outside the range.

    "},{"location":"api/validation/#textual.validation.Length.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Length.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value falls within the maximum and minimum length constraints.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Number","title":"Number class","text":"
    def __init__(\n    self,\n    minimum=None,\n    maximum=None,\n    failure_description=None,\n):\n

    Bases: Validator

    Validator that ensures the value is a number, with an optional range check.

    "},{"location":"api/validation/#textual.validation.Number.maximum","title":"maximum instance-attribute","text":"
    maximum = maximum\n

    The maximum value of the number, inclusive. If None, the maximum is unbounded.

    "},{"location":"api/validation/#textual.validation.Number.minimum","title":"minimum instance-attribute","text":"
    minimum = minimum\n

    The minimum value of the number, inclusive. If None, the minimum is unbounded.

    "},{"location":"api/validation/#textual.validation.Number.NotANumber","title":"NotANumber class","text":"

    Bases: Failure

    Indicates a failure due to the value not being a valid number (decimal/integer, inc. scientific notation)

    "},{"location":"api/validation/#textual.validation.Number.NotInRange","title":"NotInRange class","text":"

    Bases: Failure

    Indicates a failure due to the number not being within the range [minimum, maximum].

    "},{"location":"api/validation/#textual.validation.Number.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Number.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value is a valid number, optionally within a range.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Regex","title":"Regex class","text":"
    def __init__(self, regex, flags=0, failure_description=None):\n

    Bases: Validator

    A validator that checks the value matches a regex (via re.fullmatch).

    "},{"location":"api/validation/#textual.validation.Regex.flags","title":"flags instance-attribute","text":"
    flags = flags\n

    The flags to pass to re.fullmatch.

    "},{"location":"api/validation/#textual.validation.Regex.regex","title":"regex instance-attribute","text":"
    regex = regex\n

    The regex which we'll validate is matched by the value.

    "},{"location":"api/validation/#textual.validation.Regex.NoResults","title":"NoResults class","text":"

    Bases: Failure

    Indicates validation failed because the regex could not be found within the value string.

    "},{"location":"api/validation/#textual.validation.Regex.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that the value matches the regex.

    Parameters Parameter Default Description value str required

    The value that should match the regex.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.URL","title":"URL class","text":"

    Bases: Validator

    Validator that checks if a URL is valid (ensuring a scheme is present).

    "},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL class","text":"

    Bases: Failure

    Indicates that the URL is not valid.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.URL.validate","title":"validate method","text":"
    def validate(self, value):\n

    Validates that value is a valid URL (contains a scheme).

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult class","text":"

    The result of calling a Validator.validate method.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
    failure_descriptions: list[str]\n

    Utility for extracting failure descriptions as strings.

    Useful if you don't care about the additional metadata included in the Failure objects.

    Returns Type Description list[str]

    A list of the string descriptions explaining the failing validations.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failures","title":"failures instance-attribute class-attribute","text":"
    failures: Sequence[Failure] = field(default_factory=list)\n

    A list of reasons why the value was invalid. Empty if valid=True

    "},{"location":"api/validation/#textual.validation.ValidationResult.is_valid","title":"is_valid property","text":"
    is_valid: bool\n

    True if the validation was successful.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
    def failure(failures):\n

    Construct a failure ValidationResult.

    Parameters Parameter Default Description failures Sequence[Failure] required

    The failures.

    Returns Type Description ValidationResult

    A failure ValidationResult.

    "},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
    def merge(results):\n

    Merge multiple ValidationResult objects into one.

    Parameters Parameter Default Description results Sequence['ValidationResult'] required

    List of ValidationResult objects to merge.

    Returns Type Description 'ValidationResult'

    Merged ValidationResult object.

    "},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
    def success():\n

    Construct a successful ValidationResult.

    Returns Type Description ValidationResult

    A successful ValidationResult.

    "},{"location":"api/validation/#textual.validation.Validator","title":"Validator class","text":"
    def __init__(self, failure_description=None):\n

    Bases: ABC

    Base class for the validation of string values.

    Commonly used in conjunction with the Input widget, which accepts a list of validators via its constructor. This validation framework can also be used to validate any 'stringly-typed' values (for example raw command line input from sys.args).

    To implement your own Validator, subclass this class.

    Example
    class Palindrome(Validator):\n    def validate(self, value: str) -> ValidationResult:\n        def is_palindrome(value: str) -> bool:\n            return value == value[::-1]\n        return self.success() if is_palindrome(value) else self.failure(\"Not palindrome!\")\n
    "},{"location":"api/validation/#textual.validation.Validator.failure_description","title":"failure_description instance-attribute","text":"
    failure_description = failure_description\n

    A description of why the validation failed.

    The description (intended to be user-facing) to attached to the Failure if the validation fails. This failure description is ultimately accessible at the time of validation failure via the Input.Changed or Input.Submitted event, and you can access it on your message handler (a method called, for example, on_input_changed or a method decorated with @on(Input.Changed).

    "},{"location":"api/validation/#textual.validation.Validator.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Return a string description of the Failure.

    Used to provide a more fine-grained description of the failure. A Validator could fail for multiple reasons, so this method could be used to provide a different reason for different types of failure.

    Warning

    This method is only called if no other description has been supplied. If you supply a description inside a call to self.failure(description=\"...\"), or pass a description into the constructor of the validator, those will take priority, and this method won't be called.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure method","text":"
    def failure(self, description=None, value=None, failures=None):\n

    Shorthand for signaling validation failure.

    You can return failure(...) from a Validator.validate implementation to signal validation succeeded.

    Parameters Parameter Default Description description str | None None

    The failure description that will be used. When used in conjunction with the Input widget, this is the description that will ultimately be available inside the handler for Input.Changed. If not supplied, the failure_description from the Validator will be used. If that is not supplied either, then the describe_failure method on Validator will be called.

    value str | None None

    The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

    failures Failure | Sequence[Failure] | None None

    The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

    Returns Type Description ValidationResult

    A ValidationResult representing failed validation, and containing the metadata supplied to this function.

    "},{"location":"api/validation/#textual.validation.Validator.success","title":"success method","text":"
    def success(self):\n

    Shorthand for ValidationResult(True).

    You can return success() from a Validator.validate method implementation to signal that validation has succeeded.

    Returns Type Description ValidationResult

    A ValidationResult indicating validation succeeded.

    "},{"location":"api/validation/#textual.validation.Validator.validate","title":"validate abstractmethod","text":"
    def validate(self, value):\n

    Validate the value and return a ValidationResult describing the outcome of the validation.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/walk/","title":"Walk","text":"

    Functions for walking the DOM.

    Note

    For most purposes you would be better off using query, which uses these functions internally.

    "},{"location":"api/walk/#textual.walk.walk_breadth_first","title":"walk_breadth_first function","text":"
    def walk_breadth_first(\n    root, filter_type=None, *, with_root=True\n):\n

    Walk the tree breadth first (children first).

    Note

    Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

    Parameters Parameter Default Description root DOMNode required

    The root note (starting point).

    filter_type type[WalkType] | None None

    Optional DOMNode subclass to filter by, or None for no filter.

    with_root bool True

    Include the root in the walk.

    Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first function","text":"
    def walk_depth_first(root, filter_type=None, *, with_root=True):\n

    Walk the tree depth first (parents first).

    Note

    Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

    Parameters Parameter Default Description root DOMNode required

    The root note (starting point).

    filter_type type[WalkType] | None None

    Optional DOMNode subclass to filter by, or None for no filter.

    with_root bool True

    Include the root in the walk.

    Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/widget/","title":"Widget","text":"

    The base class for widgets.

    "},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount class","text":"
    def __init__(self, parent, widgets):\n

    An optional awaitable returned by mount and mount_all.

    Example
    await self.mount(Static(\"foo\"))\n
    "},{"location":"api/widget/#textual.widget.MountError","title":"MountError class","text":"

    Bases: WidgetError

    Error raised when there was a problem with the mount request.

    "},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses class","text":"

    Bases: NamedTuple

    Used for render/render_line based widgets that use caching. This structure can be used as a cache-key.

    "},{"location":"api/widget/#textual.widget.PseudoClasses.enabled","title":"enabled instance-attribute","text":"
    enabled: bool\n

    Is 'enabled' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
    focus: bool\n

    Is 'focus' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
    hover: bool\n

    Is 'hover' applied?

    "},{"location":"api/widget/#textual.widget.Widget","title":"Widget class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: DOMNode

    A Widget is the base class for Textual widgets.

    See also static for starting point for your own widgets.

    Parameters Parameter Default Description *children Widget ()

    Child widgets.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
    BORDER_SUBTITLE: str = ''\n

    Initial value for border_subtitle attribute.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
    BORDER_TITLE: str = ''\n

    Initial value for border_title attribute.

    "},{"location":"api/widget/#textual.widget.Widget.allow_horizontal_scroll","title":"allow_horizontal_scroll property","text":"
    allow_horizontal_scroll: bool\n

    Check if horizontal scroll is permitted.

    May be overridden if you want different logic regarding allowing scrolling.

    "},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
    allow_vertical_scroll: bool\n

    Check if vertical scroll is permitted.

    May be overridden if you want different logic regarding allowing scrolling.

    "},{"location":"api/widget/#textual.widget.Widget.auto_links","title":"auto_links instance-attribute class-attribute","text":"
    auto_links: Reactive[bool] = Reactive(True)\n

    Widget will highlight links automatically.

    "},{"location":"api/widget/#textual.widget.Widget.border_subtitle","title":"border_subtitle instance-attribute class-attribute","text":"
    border_subtitle: str | Text | None = _BorderTitle()\n

    A title to show in the bottom border (if there is one).

    "},{"location":"api/widget/#textual.widget.Widget.border_title","title":"border_title instance-attribute class-attribute","text":"
    border_title: str | Text | None = _BorderTitle()\n

    A title to show in the top border (if there is one).

    "},{"location":"api/widget/#textual.widget.Widget.can_focus","title":"can_focus instance-attribute class-attribute","text":"
    can_focus: bool = False\n

    Widget may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.can_focus_children","title":"can_focus_children instance-attribute class-attribute","text":"
    can_focus_children: bool = True\n

    Widget's children may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
    container_size: Size\n

    The size of the container (parent widget).

    Returns Type Description Size

    Container size.

    "},{"location":"api/widget/#textual.widget.Widget.container_viewport","title":"container_viewport property","text":"
    container_viewport: Region\n

    The viewport region (parent window).

    Returns Type Description Region

    The region that contains this widget.

    "},{"location":"api/widget/#textual.widget.Widget.content_offset","title":"content_offset property","text":"
    content_offset: Offset\n

    An offset from the Widget origin where the content begins.

    Returns Type Description Offset

    Offset from widget's origin.

    "},{"location":"api/widget/#textual.widget.Widget.content_region","title":"content_region property","text":"
    content_region: Region\n

    Gets an absolute region containing the content (minus padding and border).

    Returns Type Description Region

    Screen region that contains a widget's content.

    "},{"location":"api/widget/#textual.widget.Widget.content_size","title":"content_size property","text":"
    content_size: Size\n

    The size of the content area.

    Returns Type Description Size

    Content area size.

    "},{"location":"api/widget/#textual.widget.Widget.disabled","title":"disabled instance-attribute class-attribute","text":"
    disabled: Reactive[bool] = disabled\n

    Is the widget disabled? Disabled widgets can not be interacted with, and are typically styled to look dimmer.

    "},{"location":"api/widget/#textual.widget.Widget.dock_gutter","title":"dock_gutter property","text":"
    dock_gutter: Spacing\n

    Space allocated to docks in the parent.

    Returns Type Description Spacing

    Space to be subtracted from scrollable area.

    "},{"location":"api/widget/#textual.widget.Widget.expand","title":"expand instance-attribute class-attribute","text":"
    expand: Reactive[bool] = Reactive(False)\n

    Rich renderable may expand beyond optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
    focusable: bool\n

    Can this widget currently be focused?

    "},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
    gutter: Spacing\n

    Spacing for padding / border / scrollbars.

    Returns Type Description Spacing

    Additional spacing around content area.

    "},{"location":"api/widget/#textual.widget.Widget.has_focus","title":"has_focus instance-attribute class-attribute","text":"
    has_focus: Reactive[bool] = Reactive(False, repaint=False)\n

    Does this widget have focus? Read only.

    "},{"location":"api/widget/#textual.widget.Widget.highlight_link_id","title":"highlight_link_id instance-attribute class-attribute","text":"
    highlight_link_id: Reactive[str] = Reactive('')\n

    The currently highlighted link id. Read only.

    "},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
    horizontal_scrollbar: ScrollBar\n

    The horizontal scrollbar.

    Note

    This will create a scrollbar if one doesn't exist.

    Returns Type Description ScrollBar

    ScrollBar Widget.

    "},{"location":"api/widget/#textual.widget.Widget.hover_style","title":"hover_style instance-attribute class-attribute","text":"
    hover_style: Reactive[Style] = Reactive(\n    Style, repaint=False\n)\n

    The current hover style (style under the mouse cursor). Read only.

    "},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
    is_container: bool\n

    Is this widget a container (contains other widgets)?

    "},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scroll_end","title":"is_horizontal_scroll_end property","text":"
    is_horizontal_scroll_end: bool\n

    Is the horizontal scroll position at the maximum?

    "},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scrollbar_grabbed","title":"is_horizontal_scrollbar_grabbed property","text":"
    is_horizontal_scrollbar_grabbed: bool\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.is_mounted","title":"is_mounted property","text":"
    is_mounted: bool\n

    Check if this widget is mounted.

    "},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable: bool\n

    Can this widget be scrolled?

    "},{"location":"api/widget/#textual.widget.Widget.is_vertical_scroll_end","title":"is_vertical_scroll_end property","text":"
    is_vertical_scroll_end: bool\n

    Is the vertical scroll position at the maximum?

    "},{"location":"api/widget/#textual.widget.Widget.is_vertical_scrollbar_grabbed","title":"is_vertical_scrollbar_grabbed property","text":"
    is_vertical_scrollbar_grabbed: bool\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
    layer: str\n

    Get the name of this widgets layer.

    Returns Type Description str

    Name of layer.

    "},{"location":"api/widget/#textual.widget.Widget.layers","title":"layers property","text":"
    layers: tuple[str, ...]\n

    Layers of from parent.

    Returns Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/widget/#textual.widget.Widget.link_style","title":"link_style property","text":"
    link_style: Style\n

    Style of links.

    Returns Type Description Style

    Rich style.

    "},{"location":"api/widget/#textual.widget.Widget.link_style_hover","title":"link_style_hover property","text":"
    link_style_hover: Style\n

    Style of links underneath the mouse cursor.

    Returns Type Description Style

    Rich Style.

    "},{"location":"api/widget/#textual.widget.Widget.loading","title":"loading instance-attribute class-attribute","text":"
    loading: Reactive[bool] = Reactive(False)\n

    If set to True this widget will temporarily be replaced with a loading indicator.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
    max_scroll_x: int\n

    The maximum value of scroll_x.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_y","title":"max_scroll_y property","text":"
    max_scroll_y: int\n

    The maximum value of scroll_y.

    "},{"location":"api/widget/#textual.widget.Widget.mouse_over","title":"mouse_over instance-attribute class-attribute","text":"
    mouse_over: Reactive[bool] = Reactive(False, repaint=False)\n

    Is the mouse over this widget? Read only.

    "},{"location":"api/widget/#textual.widget.Widget.offset","title":"offset property writable","text":"
    offset: Offset\n

    Widget offset from origin.

    Returns Type Description Offset

    Relative offset.

    "},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
    opacity: float\n

    Total opacity of widget.

    "},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
    outer_size: Size\n

    The size of the widget (including padding and border).

    Returns Type Description Size

    Outer size.

    "},{"location":"api/widget/#textual.widget.Widget.region","title":"region property","text":"
    region: Region\n

    The region occupied by this widget, relative to the Screen.

    Raises Type Description NoScreen

    If there is no screen.

    errors.NoWidget

    If the widget is not on the screen.

    Returns Type Description Region

    Region within screen occupied by widget.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_offset","title":"scroll_offset property","text":"
    scroll_offset: Offset\n

    Get the current scroll offset.

    Returns Type Description Offset

    Offset a container has been scrolled by.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x instance-attribute class-attribute","text":"
    scroll_x: Reactive[float] = Reactive(\n    0.0, repaint=False, layout=False\n)\n

    The scroll position on the X axis.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_y","title":"scroll_y instance-attribute class-attribute","text":"
    scroll_y: Reactive[float] = Reactive(\n    0.0, repaint=False, layout=False\n)\n

    The scroll position on the Y axis.

    "},{"location":"api/widget/#textual.widget.Widget.scrollable_content_region","title":"scrollable_content_region property","text":"
    scrollable_content_region: Region\n

    Gets an absolute region containing the scrollable content (minus padding, border, and scrollbars).

    Returns Type Description Region

    Screen region that contains a widget's content.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
    scrollbar_corner: ScrollBarCorner\n

    The scrollbar corner.

    Note

    This will create a scrollbar corner if one doesn't exist.

    Returns Type Description ScrollBarCorner

    ScrollBarCorner Widget.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_gutter","title":"scrollbar_gutter property","text":"
    scrollbar_gutter: Spacing\n

    Spacing required to fit scrollbar(s).

    Returns Type Description Spacing

    Scrollbar gutter spacing.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_horizontal","title":"scrollbar_size_horizontal property","text":"
    scrollbar_size_horizontal: int\n

    Get the height used by the horizontal scrollbar.

    Returns Type Description int

    Number of rows in the horizontal scrollbar.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_vertical","title":"scrollbar_size_vertical property","text":"
    scrollbar_size_vertical: int\n

    Get the width used by the vertical scrollbar.

    Returns Type Description int

    Number of columns in the vertical scrollbar.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbars_enabled","title":"scrollbars_enabled property","text":"
    scrollbars_enabled: tuple[bool, bool]\n

    A tuple of booleans that indicate if scrollbars are enabled.

    Returns Type Description tuple[bool, bool]

    A tuple of (, )"},{"location":"api/widget/#textual.widget.Widget.scrollbars_space","title":"scrollbars_space property","text":"

    scrollbars_space: tuple[int, int]\n

    The number of cells occupied by scrollbars for width and height

    "},{"location":"api/widget/#textual.widget.Widget.show_horizontal_scrollbar","title":"show_horizontal_scrollbar instance-attribute class-attribute","text":"
    show_horizontal_scrollbar: Reactive[bool] = Reactive(\n    False, layout=True\n)\n

    Show a horizontal scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.show_vertical_scrollbar","title":"show_vertical_scrollbar instance-attribute class-attribute","text":"
    show_vertical_scrollbar: Reactive[bool] = Reactive(\n    False, layout=True\n)\n

    Show a vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink instance-attribute class-attribute","text":"
    shrink: Reactive[bool] = Reactive(True)\n

    Rich renderable may shrink below optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
    siblings: list[Widget]\n

    Get the widget's siblings (self is removed from the return list).

    Returns Type Description list[Widget]

    A list of siblings.

    "},{"location":"api/widget/#textual.widget.Widget.size","title":"size property","text":"
    size: Size\n

    The size of the content area.

    Returns Type Description Size

    Content area size.

    "},{"location":"api/widget/#textual.widget.Widget.tooltip","title":"tooltip property writable","text":"
    tooltip: RenderableType | None\n

    Tooltip for the widget, or None for no tooltip.

    "},{"location":"api/widget/#textual.widget.Widget.vertical_scrollbar","title":"vertical_scrollbar property","text":"
    vertical_scrollbar: ScrollBar\n

    The vertical scrollbar (create if necessary).

    Note

    This will create a scrollbar if one doesn't exist.

    Returns Type Description ScrollBar

    ScrollBar Widget.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_region","title":"virtual_region property","text":"
    virtual_region: Region\n

    The widget region relative to it's container (which may not be visible, depending on scroll offset).

    Returns Type Description Region

    The virtual region.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_region_with_margin","title":"virtual_region_with_margin property","text":"
    virtual_region_with_margin: Region\n

    The widget region relative to its container (including margin), which may not be visible, depending on the scroll offset.

    Returns Type Description Region

    The virtual region of the Widget, inclusive of its margin.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_size","title":"virtual_size instance-attribute class-attribute","text":"
    virtual_size: Reactive[Size] = Reactive(\n    Size(0, 0), layout=True\n)\n

    The virtual (scrollable) size of the widget.

    "},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
    visible_siblings: list[Widget]\n

    A list of siblings which will be shown.

    Returns Type Description list[Widget]

    List of siblings.

    "},{"location":"api/widget/#textual.widget.Widget.window_region","title":"window_region property","text":"
    window_region: Region\n

    The region within the scrollable area that is currently visible.

    Returns Type Description Region

    New region.

    "},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation. Defaults to value if not set.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print method","text":"
    def begin_capture_print(self, stdout=True, stderr=True):\n

    Capture text from print statements (or writes to stdout / stderr).

    If printing is captured, the widget will be sent an events.Print message.

    Call end_capture_print to disable print capture.

    Parameters Parameter Default Description stdout bool True

    Whether to capture stdout.

    stderr bool True

    Whether to capture stderr.

    "},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur method","text":"
    def blur(self):\n

    Blur (un-focus) the widget.

    Focus will be moved to the next available widget in the focus chain..

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.can_view","title":"can_view method","text":"
    def can_view(self, widget):\n

    Check if a given widget is in the current view (scrollable area).

    Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

    Parameters Parameter Default Description widget Widget required

    A widget that is a descendant of self.

    Returns Type Description bool

    True if the entire widget is in view, False if it is partially visible or not in view.

    "},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse method","text":"
    def capture_mouse(self, capture=True):\n

    Capture (or release) the mouse.

    When captured, mouse events will go to this widget even when the pointer is not directly over the widget.

    Parameters Parameter Default Description capture bool True

    True to capture or False to release.

    "},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled method","text":"
    def check_message_enabled(self, message):\n

    Check if a given message is enabled (allowed to be sent).

    Parameters Parameter Default Description message Message required

    A message object

    Returns Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose method","text":"
    def compose(self):\n

    Called by Textual to create child widgets.

    Extend this to build a UI.

    Example
    def compose(self) -> ComposeResult:\n    yield Header()\n    yield Label(\"Press the button below:\")\n    yield Button()\n    yield Footer()\n
    "},{"location":"api/widget/#textual.widget.Widget.compose_add_child","title":"compose_add_child method","text":"
    def compose_add_child(self, widget):\n

    Add a node to children.

    This is used by the compose process when it adds children. There is no need to use it directly, but you may want to override it in a subclass if you want children to be attached to a different node.

    Parameters Parameter Default Description widget Widget required

    A Widget to add.

    "},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print method","text":"
    def end_capture_print(self):\n

    End print capture (set with begin_capture_print).

    "},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus method","text":"
    def focus(self, scroll_visible=True):\n

    Give focus to this widget.

    Parameters Parameter Default Description scroll_visible bool True

    Scroll parent to make this widget visible.

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id method","text":"
    def get_child_by_id(self, id, expect_type=None):\n

    Return the first child (immediate descendent) of this node with the given ID.

    Parameters Parameter Default Description id str required

    The ID of the child.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type.

    Returns Type Description ExpectType | Widget

    The first child of this node with the ID.

    Raises Type Description NoMatches

    if no children could be found for this ID

    WrongType

    if the wrong type was found.

    "},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type method","text":"
    def get_child_by_type(self, expect_type):\n

    Get the first immediate child of a given type.

    Only returns exact matches, and so will not match subclasses of the given type.

    Parameters Parameter Default Description expect_type type[ExpectType] required

    The type of the child to search for.

    Raises Type Description NoMatches

    If no matching child is found.

    Returns Type Description ExpectType

    The first immediate child widget with the expected type.

    "},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style method","text":"
    def get_component_rich_style(self, name, *, partial=False):\n

    Get a Rich style for a component.

    Parameters Parameter Default Description name str required

    Name of component.

    partial bool False

    Return a partial style (not combined with parent).

    Returns Type Description Style

    A Rich style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height method","text":"
    def get_content_height(self, container, viewport, width):\n

    Called by Textual to get the height of the content area. May be overridden in a subclass.

    Parameters Parameter Default Description container Size required

    Size of the container (immediate parent) widget.

    viewport Size required

    Size of the viewport.

    width int required

    Width of renderable.

    Returns Type Description int

    The height of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width method","text":"
    def get_content_width(self, container, viewport):\n

    Called by textual to get the width of the content area. May be overridden in a subclass.

    Parameters Parameter Default Description container Size required

    Size of the container (immediate parent) widget.

    viewport Size required

    Size of the viewport.

    Returns Type Description int

    The optimal width of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_loading_widget","title":"get_loading_widget method","text":"
    def get_loading_widget(self):\n

    Get a widget to display a loading indicator.

    The default implementation will defer to App.get_loading_widget.

    Returns Type Description Widget

    A widget in place of this widget to indicate a loading.

    "},{"location":"api/widget/#textual.widget.Widget.get_pseudo_class_state","title":"get_pseudo_class_state method","text":"
    def get_pseudo_class_state(self):\n

    Get an object describing whether each pseudo class is present on this object or not.

    Returns Type Description PseudoClasses

    A PseudoClasses object describing the pseudo classes that are present.

    "},{"location":"api/widget/#textual.widget.Widget.get_pseudo_classes","title":"get_pseudo_classes method","text":"
    def get_pseudo_classes(self):\n

    Pseudo classes for a widget.

    Returns Type Description Iterable[str]

    Names of the pseudo classes.

    "},{"location":"api/widget/#textual.widget.Widget.get_style_at","title":"get_style_at method","text":"
    def get_style_at(self, x, y):\n

    Get the Rich style in a widget at a given relative offset.

    Parameters Parameter Default Description x int required

    X coordinate relative to the widget.

    y int required

    Y coordinate relative to the widget.

    Returns Type Description Style

    A rich Style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id method","text":"
    def get_widget_by_id(self, id, expect_type=None):\n

    Return the first descendant widget with the given ID.

    Performs a depth-first search rooted at this widget.

    Parameters Parameter Default Description id str required

    The ID to search for in the subtree.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type.

    Returns Type Description ExpectType | Widget

    The first descendant encountered with this ID.

    Raises Type Description NoMatches

    if no children could be found for this ID.

    WrongType

    if the wrong type was found.

    "},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount method","text":"
    def mount(self, *widgets, before=None, after=None):\n

    Mount widgets below this widget (making this widget a container).

    Parameters Parameter Default Description *widgets Widget ()

    The widget(s) to mount.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all method","text":"
    def mount_all(self, widgets, *, before=None, after=None):\n

    Mount widgets from an iterable.

    Parameters Parameter Default Description widgets Iterable[Widget] required

    An iterable of widgets.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets","title":"mount_composed_widgets async","text":"
    def mount_composed_widgets(self, widgets):\n

    Called by Textual to mount widgets after compose.

    There is generally no need to implement this method in your application. See Lazy for a class which uses this method to implement lazy mounting.

    Parameters Parameter Default Description widgets list[Widget] required

    A list of child widgets.

    "},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child method","text":"
    def move_child(self, child, *, before=None, after=None):\n

    Move a child widget within its parent's list of children.

    Parameters Parameter Default Description child int | Widget required

    The child widget to move.

    before int | Widget | None None

    Child widget or location index to move before.

    after int | Widget | None None

    Child widget or location index to move after.

    Raises Type Description WidgetError

    If there is a problem with the child or target.

    Note

    Only one of before or after can be provided. If neither or both are provided a WidgetError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify method","text":"
    def notify(\n    self,\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=Notification.timeout\n):\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters Parameter Default Description message str required

    The message for the notification.

    title str ''

    The title for the notification.

    severity SeverityLevel 'information'

    The severity of the notification.

    timeout float Notification.timeout

    The timeout for the notification.

    See App.notify for the full documentation for this method.

    "},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message method","text":"
    def post_message(self, message):\n

    Post a message to this widget.

    Parameters Parameter Default Description message Message required

    Message to post.

    Returns Type Description bool

    True if the message was posted, False if this widget was closed / closing.

    "},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render method","text":"
    def post_render(self, renderable):\n

    Applies style attributes to the default renderable.

    Returns Type Description ConsoleRenderable

    A new renderable.

    "},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh method","text":"
    def refresh(self, *regions, repaint=True, layout=False):\n

    Initiate a refresh of the widget.

    This method sets an internal flag to perform a refresh, which will be done on the next idle event. Only one refresh will be done even if this method is called multiple times.

    By default this method will cause the content of the widget to refresh, but not change its size. You can also set layout=True to perform a layout.

    Warning

    It is rarely necessary to call this method explicitly. Updating styles or reactive attributes will do this automatically.

    Parameters Parameter Default Description *regions Region ()

    Additional screen regions to mark as dirty.

    repaint bool True

    Repaint the widget (will call render() again).

    layout bool False

    Also layout widgets in the view.

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse method","text":"
    def release_mouse(self):\n

    Release the mouse.

    Mouse events will only be sent when the mouse is over the widget.

    "},{"location":"api/widget/#textual.widget.Widget.remove","title":"remove method","text":"
    def remove(self):\n

    Remove the Widget from the DOM (effectively deleting it).

    Returns Type Description AwaitRemove

    An awaitable object that waits for the widget to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.remove_children","title":"remove_children method","text":"
    def remove_children(self):\n

    Remove all children of this Widget from the DOM.

    Returns Type Description AwaitRemove

    An awaitable object that waits for the children to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.render","title":"render method","text":"
    def render(self):\n

    Get text or Rich renderable for this widget.

    Implement this for custom widgets.

    Example
    from textual.app import RenderableType\nfrom textual.widget import Widget\n\nclass CustomWidget(Widget):\n    def render(self) -> RenderableType:\n        return \"Welcome to [bold red]Textual[/]!\"\n
    Returns Type Description RenderableType

    Any renderable.

    "},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line method","text":"
    def render_line(self, y):\n

    Render a line of content.

    Parameters Parameter Default Description y int required

    Y Coordinate of line.

    Returns Type Description Strip

    A rendered line.

    "},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines method","text":"
    def render_lines(self, crop):\n

    Render the widget in to lines.

    Parameters Parameter Default Description crop Region required

    Region within visible area to render.

    Returns Type Description list[Strip]

    A list of list of segments.

    "},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str method","text":"
    def render_str(self, text_content):\n

    Convert str in to a Text object.

    If you pass in an existing Text object it will be returned unaltered.

    Parameters Parameter Default Description text_content str | Text required

    Text or str.

    Returns Type Description Text

    A text object.

    "},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
    def run_action(self, action):\n

    Perform a given action, with this widget as the default namespace.

    Parameters Parameter Default Description action str required

    Action encoded as a string.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down method","text":"
    def scroll_down(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one line down.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end method","text":"
    def scroll_end(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to the end of the container.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home method","text":"
    def scroll_home(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to home position.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left method","text":"
    def scroll_left(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one cell left.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down method","text":"
    def scroll_page_down(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page down.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left method","text":"
    def scroll_page_left(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page left.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right method","text":"
    def scroll_page_right(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page right.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up method","text":"
    def scroll_page_up(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page up.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative method","text":"
    def scroll_relative(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll relative to current position.

    Parameters Parameter Default Description x float | None None

    X distance (columns) to scroll, or None for no change.

    y float | None None

    Y distance (rows) to scroll, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True. Or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right method","text":"
    def scroll_right(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one cell right.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to method","text":"
    def scroll_to(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters Parameter Default Description x float | None None

    X coordinate (column) to scroll to, or None for no change.

    y float | None None

    Y coordinate (row) to scroll to, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Note

    The call to scroll is made after the next refresh.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center method","text":"
    def scroll_to_center(\n    self,\n    widget,\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    origin_visible=True,\n    on_complete=None\n):\n

    Scroll this widget to the center of self.

    The center of the widget will be scrolled to the center of the container.

    Parameters Parameter Default Description widget Widget required

    The widget to scroll to the center of self.

    animate bool True

    Whether to animate the scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    origin_visible bool True

    Ensure that the top left corner of the widget remains visible after the scroll.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region method","text":"
    def scroll_to_region(\n    self,\n    region,\n    *,\n    spacing=None,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    center=False,\n    top=False,\n    origin_visible=True,\n    force=False,\n    on_complete=None\n):\n

    Scrolls a given region in to view, if required.

    This method will scroll the least distance required to move region fully within the scrollable area.

    Parameters Parameter Default Description region Region required

    A region that should be visible.

    spacing Spacing | None None

    Optional spacing around the region.

    animate bool True

    True to animate, or False to jump.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    top bool False

    Scroll region to top of container.

    origin_visible bool True

    Ensure that the top left of the widget is within the window.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Returns Type Description Offset

    The distance that was scrolled.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget method","text":"
    def scroll_to_widget(\n    self,\n    widget,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    center=False,\n    top=False,\n    origin_visible=True,\n    force=False,\n    on_complete=None\n):\n

    Scroll scrolling to bring a widget in to view.

    Parameters Parameter Default Description widget Widget required

    A descendant widget.

    animate bool True

    True to animate, or False to jump.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    top bool False

    Scroll widget to top of container.

    origin_visible bool True

    Ensure that the top left of the widget is within the window.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Returns Type Description bool

    True if any scrolling has occurred in any descendant, otherwise False.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up method","text":"
    def scroll_up(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one line up.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible method","text":"
    def scroll_visible(\n    self,\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    top=False,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll the container to make this widget visible.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    top bool False

    Scroll to top of container.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.set_loading","title":"set_loading method","text":"
    def set_loading(self, loading):\n

    Set or reset the loading state of this widget.

    A widget in a loading state will display a LoadingIndicator that obscures the widget.

    Parameters Parameter Default Description loading bool required

    True to put the widget into a loading state, or False to reset the loading state.

    Returns Type Description Awaitable

    An optional awaitable.

    "},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
    def stop_animation(self, attribute, complete=True):\n

    Stop an animation on an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute whose animation should be stopped.

    complete bool True

    Should the animation be set to its final value?

    Note

    If there is no animation scheduled or running, this is a no-op.

    "},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled method","text":"
    def watch_disabled(self):\n

    Update the styles of the widget and its children when disabled is toggled.

    "},{"location":"api/widget/#textual.widget.Widget.watch_has_focus","title":"watch_has_focus method","text":"
    def watch_has_focus(self, value):\n

    Update from CSS if has focus state changes.

    "},{"location":"api/widget/#textual.widget.Widget.watch_mouse_over","title":"watch_mouse_over method","text":"
    def watch_mouse_over(self, value):\n

    Update from CSS if mouse over state changes.

    "},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError class","text":"

    Bases: Exception

    Base widget error.

    "},{"location":"api/work/","title":"Work","text":"

    A decorator used to create workers.

    Parameters Parameter Default Description method Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None None

    A function or coroutine.

    name str ''

    A short string to identify the worker (in logs and debugging).

    group str 'default'

    A short string to identify a group of workers.

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    exclusive bool False

    Cancel all workers in the same group.

    description str | None None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    thread bool False

    Mark the method as a thread worker.

    "},{"location":"api/worker/","title":"Worker","text":"

    A class to manage concurrent work.

    "},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
    WorkType: TypeAlias = Union[\n    Callable[[], Coroutine[None, None, ResultType]],\n    Callable[[], ResultType],\n    Awaitable[ResultType],\n]\n

    Type used for workers.

    "},{"location":"api/worker/#textual.worker.active_worker","title":"active_worker module-attribute","text":"
    active_worker: ContextVar[Worker] = ContextVar(\n    \"active_worker\"\n)\n

    Currently active worker context var.

    "},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError class","text":"

    Bases: WorkerError

    The operation would result in a deadlock.

    "},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker class","text":"

    Bases: Exception

    There is no active worker.

    "},{"location":"api/worker/#textual.worker.Worker","title":"Worker class","text":"
    def __init__(\n    self,\n    node,\n    work=None,\n    *,\n    name=\"\",\n    group=\"default\",\n    description=\"\",\n    exit_on_error=True,\n    thread=False\n):\n

    Bases: Generic[ResultType]

    A class to manage concurrent work (either a task or a thread).

    Parameters Parameter Default Description node DOMNode required

    The widget, screen, or App that initiated the work.

    work WorkType | None None

    A callable, coroutine, or other awaitable object to run in the worker.

    name str ''

    Name of the worker (short string to help identify when debugging).

    group str 'default'

    The worker group.

    description str ''

    Description of the worker (longer string with more details).

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    thread bool False

    Mark the worker as a thread worker.

    "},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
    completed_steps: int\n

    The number of completed steps.

    "},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
    error: BaseException | None\n

    The exception raised by the worker, or None if there was no error.

    "},{"location":"api/worker/#textual.worker.Worker.is_cancelled","title":"is_cancelled property","text":"
    is_cancelled: bool\n

    Has the work been cancelled?

    Note that cancelled work may still be running.

    "},{"location":"api/worker/#textual.worker.Worker.is_finished","title":"is_finished property","text":"
    is_finished: bool\n

    Has the task finished (cancelled, error, or success)?

    "},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
    is_running: bool\n

    Is the task running?

    "},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
    node: DOMNode\n

    The node where this worker was run from.

    "},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
    progress: float\n

    Progress as a percentage.

    If the total steps is None, then this will return 0. The percentage will be clamped between 0 and 100.

    "},{"location":"api/worker/#textual.worker.Worker.result","title":"result property","text":"
    result: ResultType | None\n

    The result of the worker, or None if there is no result.

    "},{"location":"api/worker/#textual.worker.Worker.state","title":"state property writable","text":"
    state: WorkerState\n

    The current state of the worker.

    "},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
    total_steps: int | None\n

    The number of total steps, or None if indeterminate.

    "},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged class","text":"
    def __init__(self, worker, state):\n

    Bases: Message

    The worker state changed.

    Parameters Parameter Default Description worker Worker required

    The worker object.

    state WorkerState required

    New state.

    "},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance method","text":"
    def advance(self, steps=1):\n

    Advance the number of completed steps.

    Parameters Parameter Default Description steps int 1

    Number of steps to advance.

    "},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel method","text":"
    def cancel(self):\n

    Cancel the task.

    "},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
    def run(self):\n

    Run the work.

    Implement this method in a subclass, or pass a callable to the constructor.

    Returns Type Description ResultType

    Return value of the work.

    "},{"location":"api/worker/#textual.worker.Worker.update","title":"update method","text":"
    def update(self, completed_steps=None, total_steps=-1):\n

    Update the number of completed steps.

    Parameters Parameter Default Description completed_steps int | None None

    The number of completed seps, or None to not change.

    total_steps int | None -1

    The total number of steps, None for indeterminate, or -1 to leave unchanged.

    "},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
    def wait(self):\n

    Wait for the work to complete.

    Raises Type Description WorkerFailed

    If the Worker raised an exception.

    WorkerCancelled

    If the Worker was cancelled before it completed.

    Returns Type Description ResultType

    The return value of the work.

    "},{"location":"api/worker/#textual.worker.WorkerCancelled","title":"WorkerCancelled class","text":"

    Bases: WorkerError

    The worker was cancelled and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError class","text":"

    Bases: Exception

    A worker related error.

    "},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed class","text":"
    def __init__(self, error):\n

    Bases: WorkerError

    The worker raised an exception and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState class","text":"

    Bases: enum.Enum

    A description of the worker's current state.

    "},{"location":"api/worker/#textual.worker.WorkerState.CANCELLED","title":"CANCELLED instance-attribute class-attribute","text":"
    CANCELLED = 3\n

    Worker is not running, and was cancelled.

    "},{"location":"api/worker/#textual.worker.WorkerState.ERROR","title":"ERROR instance-attribute class-attribute","text":"
    ERROR = 4\n

    Worker is not running, and exited with an error.

    "},{"location":"api/worker/#textual.worker.WorkerState.PENDING","title":"PENDING instance-attribute class-attribute","text":"
    PENDING = 1\n

    Worker is initialized, but not running.

    "},{"location":"api/worker/#textual.worker.WorkerState.RUNNING","title":"RUNNING instance-attribute class-attribute","text":"
    RUNNING = 2\n

    Worker is running.

    "},{"location":"api/worker/#textual.worker.WorkerState.SUCCESS","title":"SUCCESS instance-attribute class-attribute","text":"
    SUCCESS = 5\n

    Worker is not running, and completed successfully.

    "},{"location":"api/worker/#textual.worker.get_current_worker","title":"get_current_worker function","text":"
    def get_current_worker():\n

    Get the currently active worker.

    Raises Type Description NoActiveWorker

    If there is no active worker.

    Returns Type Description Worker

    A Worker instance.

    "},{"location":"api/worker_manager/","title":"Worker manager","text":"

    A class to manage workers for an app.

    You access this object via App.workers or Widget.workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager","title":"WorkerManager class","text":"
    def __init__(self, app):\n

    An object to manager a number of workers.

    You will not have to construct this class manually, as widgets, screens, and apps have a worker manager accessibly via a workers attribute.

    Parameters Parameter Default Description app App required

    An App instance.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.add_worker","title":"add_worker method","text":"
    def add_worker(self, worker, start=True, exclusive=True):\n

    Add a new worker.

    Parameters Parameter Default Description worker Worker required

    A Worker instance.

    start bool True

    Start the worker if True, otherwise the worker must be started manually.

    exclusive bool True

    Cancel all workers in the same group as worker.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_all","title":"cancel_all method","text":"
    def cancel_all(self):\n

    Cancel all workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_group","title":"cancel_group method","text":"
    def cancel_group(self, node, group):\n

    Cancel a single group.

    Parameters Parameter Default Description node DOMNode required

    Worker DOM node.

    group str required

    A group name.

    Returns Type Description list[Worker]

    A list of workers that were cancelled.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_node","title":"cancel_node method","text":"
    def cancel_node(self, node):\n

    Cancel all workers associated with a given node

    Parameters Parameter Default Description node DOMNode required

    A DOM node (widget, screen, or App).

    Returns Type Description list[Worker]

    List of cancelled workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.start_all","title":"start_all method","text":"
    def start_all(self):\n

    Start all the workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
    def wait_for_complete(self, workers=None):\n

    Wait for workers to complete.

    Parameters Parameter Default Description workers Iterable[Worker] | None None

    An iterable of workers or None to wait for all workers in the manager.

    "},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2023/03/15/no-async-async-with-python/","title":"No-async async with Python","text":"

    A (reasonable) criticism of async is that it tends to proliferate in your code. In order to await something, your functions must be async all the way up the call-stack. This tends to result in you making things async just to support that one call that needs it or, worse, adding async just-in-case. Given that going from def to async def is a breaking change there is a strong incentive to go straight there.

    Before you know it, you have adopted a policy of \"async all the things\".

    Textual is an async framework, but doesn't require the app developer to use the async and await keywords (but you can if you need to). This post is about how Textual accomplishes this async-agnosticism.

    Info

    See this example from the docs for an async-less Textual app.

    "},{"location":"blog/2023/03/15/no-async-async-with-python/#an-apology","title":"An apology","text":"

    But first, an apology! In a previous post I said Textual \"doesn't do any IO of its own\". This is not accurate. Textual responds to keys and mouse events (Input) and writes content to the terminal (Output).

    Although Textual clearly does do IO, it uses asyncio mainly for concurrency. It allows each widget to update its part of the screen independently from the rest of the app.

    "},{"location":"blog/2023/03/15/no-async-async-with-python/#await-me-maybe","title":"Await me (maybe)","text":"

    The first no-async async technique is the \"Await me maybe\" pattern, a term first coined by Simon Willison. This is particularly applicable to callbacks (or in Textual terms, message handlers).

    The await_me_maybe function below can run a callback that is either a plain old function or a coroutine (async def). It does this by awaiting the result of the callback if it is awaitable, or simply returning the result if it is not.

    import asyncio\nimport inspect\n\n\ndef plain_old_function():\n    return \"Plain old function\"\n\nasync def async_function():\n    return \"Async function\"\n\n\nasync def await_me_maybe(callback):\n    result = callback()\n    if inspect.isawaitable(result):\n        return await result\n    return result\n\n\nasync def run_framework():\n    print(\n        await await_me_maybe(plain_old_function)\n    )\n    print(\n        await await_me_maybe(async_function)\n    )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run_framework())\n
    "},{"location":"blog/2023/03/15/no-async-async-with-python/#optionally-awaitable","title":"Optionally awaitable","text":"

    The \"await me maybe\" pattern is great when an async framework calls the app's code. The app developer can choose to write async code or not. Things get a little more complicated when the app wants to call the framework's API. If the API has asynced all the things, then it would force the app to do the same.

    Textual's API consists of regular methods for the most part, but there are a few methods which are optionally awaitable. These are not coroutines (which must be awaited to do anything).

    In practice, this means that those API calls initiate something which will complete a short time later. If you discard the return value then it won't prevent it from working. You only need to await if you want to know when it has finished.

    The mount method is one such method. Calling it will add a widget to the screen:

    def on_key(self):\n    # Add MyWidget to the screen\n    self.mount(MyWidget(\"Hello, World!\"))\n

    In this example we don't care that the widget hasn't been mounted immediately, only that it will be soon.

    Note

    Textual awaits the result of mount after the message handler, so even if you don't explicitly await it, it will have been completed by the time the next message handler runs.

    We might care if we want to mount a widget then make some changes to it. By making the handler async and awaiting the result of mount, we can be sure that the widget has been initialized before we update it:

    async def on_key(self):\n    # Add MyWidget to the screen\n    await self.mount(MyWidget(\"Hello, World!\"))\n    # add a border\n    self.query_one(MyWidget).styles.border = (\"heavy\", \"red\")\n

    Incidentally, I found there were very few examples of writing awaitable objects in Python. So here is the code for AwaitMount which is returned by the mount method:

    class AwaitMount:\n    \"\"\"An awaitable returned by mount() and mount_all().\"\"\"\n\n    def __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:\n        self._parent = parent\n        self._widgets = widgets\n\n    async def __call__(self) -> None:\n        \"\"\"Allows awaiting via a call operation.\"\"\"\n        await self\n\n    def __await__(self) -> Generator[None, None, None]:\n        async def await_mount() -> None:\n            if self._widgets:\n                aws = [\n                    create_task(widget._mounted_event.wait(), name=\"await mount\")\n                    for widget in self._widgets\n                ]\n                if aws:\n                    await wait(aws)\n                    self._parent.refresh(layout=True)\n\n        return await_mount().__await__()\n
    "},{"location":"blog/2023/03/15/no-async-async-with-python/#summing-up","title":"Summing up","text":"

    Textual did initially \"async all the things\", which you might see if you find some old Textual code. Now async is optional.

    This is not because I dislike async. I'm a fan! But it does place a small burden on the developer (more to type and think about). With the current API you generally don't need to write coroutines, or remember to await things. But async is there if you need it.

    We're finding that Textual is increasingly becoming a UI to things which are naturally concurrent, so async was a good move. Concurrency can be a tricky subject, so we're planning some API magic to take the pain out of running tasks, threads, and processes. Stay tuned!

    Join us on our Discord server if you want to talk about these things with the Textualize developers.

    "},{"location":"blog/2022/12/08/be-the-keymaster/","title":"Be the Keymaster!","text":""},{"location":"blog/2022/12/08/be-the-keymaster/#that-didnt-go-to-plan","title":"That didn't go to plan","text":"

    So... yeah... the blog. When I wrote my previous (and first) post I had wanted to try and do a post towards the end of each week, highlighting what I'd done on the \"dogfooding\" front. Life kinda had other plans. Not in a terrible way, but it turns out that getting both flu and Covid jabs (AKA \"jags\" as they tend to say in my adopted home) on the same day doesn't really agree with me too well.

    I have been working, but there's been some odd moments in the past week and a bit and, last week, once I got to the end, I was glad for it to end. So no blog post happened.

    Anyway...

    "},{"location":"blog/2022/12/08/be-the-keymaster/#what-have-i-been-up-to","title":"What have I been up to?","text":"

    While mostly sat feeling sorry for myself on my sofa, I have been coding. Rather than list all the different things here in detail, I'll quickly mention them with links to where to find them and play with them if you want:

    "},{"location":"blog/2022/12/08/be-the-keymaster/#fivepyfive","title":"FivePyFive","text":"

    While my Textual 5x5 puzzle is one of the examples in the Textual repo, I wanted to make it more widely available so people can download it with pip or pipx. See over on PyPi and see if you can solve it. ;-)

    "},{"location":"blog/2022/12/08/be-the-keymaster/#textual-qrcode","title":"textual-qrcode","text":"

    I wanted to put together a very small example of how someone may put together a third party widget library, and in doing so selected what I thought was going to be a mostly-useless example: a wrapper around a text-based QR code generator website. Weirdly I've had a couple of people express a need for QR codes in the terminal since publishing that!

    "},{"location":"blog/2022/12/08/be-the-keymaster/#pispy","title":"PISpy","text":"

    PISpy is a very simple terminal-based client for the PyPi API. Mostly it provides a hypertext interface to Python package details, letting you look up a package and then follow its dependency links. It's very simple at the moment, but I think more fun things can be done with this.

    "},{"location":"blog/2022/12/08/be-the-keymaster/#oidia","title":"OIDIA","text":"

    I'm a big fan of the use of streak-tracking in one form or another. Personally I use a streak-tracking app for keeping tabs of all sorts of good (and bad) habits, and as a heavy user of all things Apple I make a lot of use of the Fitness rings, etc. So I got to thinking it might be fun to do a really simple, no shaming, no counting, just recording, steak app for the Terminal. OIDIA is the result.

    As of the time of writing I only finished the first version of this yesterday evening, so there are plenty of rough edges; but having got it to a point where it performed the basic tasks I wanted from it, that seemed like a good time to publish.

    Expect to see this getting more updates and polish.

    "},{"location":"blog/2022/12/08/be-the-keymaster/#wait-what-about-this-keymaster-thing","title":"Wait, what about this Keymaster thing?","text":"

    Ahh, yes, about that... So one of the handy things I'm finding about Textual is its key binding system. The more I build Textual apps, the more I appreciate the bindings, how they can be associated with specific widgets, the use of actions (which can be used from other places too), etc.

    But... (there's always a \"but\" right -- I mean, there'd be no blog post to be had here otherwise).

    The terminal doesn't have access to all the key combinations you may want to use, and also, because some keys can't necessarily be \"typed\", at least not easily (think about it: there's no F1 character, you have to type F1), many keys and key combinations need to be bound with specific names.

    So there's two problems here: how do I discover what keys even turn up in my application, and when they do, what should I call them when I pass them to Binding?

    That felt like a \"well Dave just build an app for it!\" problem. So I did:

    If you're building apps with Textual and you want to discover what keys turn up from your terminal and are available to your application, you can:

    $ pipx install textual-keys\n

    and then just run textual-keys and start mashing the keyboard to find out.

    There's a good chance that this app, or at least a version of it, will make it into Textual itself (very likely as one of the devtools). But for now it's just an easy install away.

    I think there's a call to be made here too: have you built anything to help speed up how you work with Textual, or just make the development experience \"just so\"? If so, do let us know, and come yell about it on the #show-and-tell channel in our Discord server.

    "},{"location":"blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/","title":"A better asyncio sleep for Windows to fix animation","text":"

    I spent some time optimizing Textual on Windows recently, and discovered something which may be of interest to anyone working with async code on that platform.

    Animation, scrolling, and fading had always been unsatisfactory on Windows. Textual was usable, but the lag when scrolling made apps feel far less snappy that other platforms. On macOS and Linux, scrolling is fast enough that it feels close to a native app, not something running in a terminal. Yet the Windows experience never improved, even as Textual got faster with each release.

    I had chalked this up to Windows Terminal being slow to render updates. After all, the classic Windows terminal was (and still is) glacially slow. Perhaps Microsoft just weren't focusing on performance.

    In retrospect, that was highly improbable. Like all modern terminals, Windows Terminal uses the GPU to render updates. Even without focussing on performance, it should be fast.

    I figured I'd give it one last attempt to speed up Textual on Windows. If I failed, Windows would forever be a third-class platform for Textual apps.

    It turned out that it was nothing to do with performance, per se. The issue was with a single asyncio function: asyncio.sleep.

    Textual has a Timer class which creates events at regular intervals. It powers the JS-like set_interval and set_timer functions. It is also used internally to do animation (such as smooth scrolling). This Timer class calls asyncio.sleep to wait the time between one event and the next.

    On macOS and Linux, calling asynco.sleep is fairly accurate. If you call sleep(3.14), it will return within 1% of 3.14 seconds. This is not the case for Windows, which for historical reasons uses a timer with a granularity of 15 milliseconds. The upshot is that sleep times will be rounded up to the nearest multiple of 15 milliseconds.

    This limit appears to hold true for all async primitives on Windows. If you wait for something with a timeout, it will return on a multiple of 15 milliseconds. Fortunately there is work in the CPython pipeline to make this more accurate. Thanks to Steve Dower for pointing this out.

    This lack of accuracy in the timer meant that timer events were created at a far slower rate than intended. Animation was slower because Textual was waiting too long between updates.

    Once I had figured that out, I needed an alternative to asyncio.sleep for Textual's Timer class. And I found one. The following version of sleep is accurate to well within 1%:

    from time import sleep as time_sleep\nfrom asyncio import get_running_loop\n\nasync def sleep(sleep_for: float) -> None:\n    \"\"\"An asyncio sleep.\n\n    On Windows this achieves a better granularity than asyncio.sleep\n\n    Args:\n        sleep_for (float): Seconds to sleep for.\n    \"\"\"    \n    await get_running_loop().run_in_executor(None, time_sleep, sleep_for)\n

    That is a drop-in replacement for sleep on Windows. With it, Textual runs a lot smoother. Easily on par with macOS and Linux.

    It's not quite perfect. There is a little tearing during full \"screen\" updates, but performance is decent all round. I suspect when this bug is fixed (big thanks to Paul Moore for looking in to that), and Microsoft implements this protocol then Textual on Windows will be A+.

    This Windows improvement will be in v0.9.0 of Textual, which will be released in a few days.

    "},{"location":"blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/","title":"The Heisenbug lurking in your async code","text":"

    I'm taking a brief break from blogging about Textual to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this tweet.

    If you have ever used asyncio.create_task you may have created a bug for yourself that is challenging (read almost impossible) to reproduce. If it occurs, your code will likely fail in unpredictable ways.

    The root cause of this Heisenbug is that if you don't hold a reference to the task object returned by create_task then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.

    This behavior is well documented, as you can see from this excerpt (emphasis mine):

    But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a lot of projects where this bug is waiting for just the right moment to ruin somebody's day.

    I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as \"daemon\" threads they will exist for the lifetime of your app. Not so with Tasks.

    The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use TaskGroups which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine.

    "},{"location":"blog/2023/03/08/overhead-of-python-asyncio-tasks/","title":"Overhead of Python Asyncio tasks","text":"

    Every widget in Textual, be it a button, tree view, or a text input, runs an asyncio task. There is even a task for scrollbar corners (the little space formed when horizontal and vertical scrollbars meet).

    Info

    It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. Those tasks are used to power message queues, so that widgets (UI components) can do whatever they do at their own pace.

    Its fair to say that Textual apps launch a lot of tasks. Which is why when I was trying to optimize startup (for apps with 1000s of widgets) I suspected it was task related.

    I needed to know how much of an overhead it was to launch tasks. Tasks are lighter weight than threads, but how much lighter? The only way to know for certain was to profile.

    The following code launches a load of do nothing tasks, then waits for them to shut down. This would give me an idea of how performant create_task is, and also a baseline for optimizations. I would know the absolute limit of any optimizations I make.

    from asyncio import create_task, wait, run\nfrom time import process_time as time\n\n\nasync def time_tasks(count=100) -> float:\n    \"\"\"Time creating and destroying tasks.\"\"\"\n\n    async def nop_task() -> None:\n        \"\"\"Do nothing task.\"\"\"\n        pass\n\n    start = time()\n    tasks = [create_task(nop_task()) for _ in range(count)]\n    await wait(tasks)\n    elapsed = time() - start\n    return elapsed\n\n\nfor count in range(100_000, 1000_000 + 1, 100_000):\n    create_time = run(time_tasks(count))\n    create_per_second = 1 / (create_time / count)\n    print(f\"{count:,} tasks \\t {create_per_second:0,.0f} tasks per/s\")\n

    And here is the output:

    100,000 tasks    280,003 tasks per/s\n200,000 tasks    255,275 tasks per/s\n300,000 tasks    248,713 tasks per/s\n400,000 tasks    248,383 tasks per/s\n500,000 tasks    241,624 tasks per/s\n600,000 tasks    260,660 tasks per/s\n700,000 tasks    244,510 tasks per/s\n800,000 tasks    247,455 tasks per/s\n900,000 tasks    242,744 tasks per/s\n1,000,000 tasks          259,715 tasks per/s\n

    Info

    Running on an M1 MacBook Pro.

    This tells me I can create, run, and shutdown 260K tasks per second.

    That's fast.

    Clearly create_task is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). I've noticed some big wins there, so the next version of Textual will be faster to start apps with a metric tonne of widgets.

    But I still need to know what to do with those scrollbar corners. A task for two characters. I don't even...

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/","title":"A year of building for the terminal","text":"

    I joined Textualize back in January 2022, and since then have been hard at work with the team on both Rich and Textual. Over the course of the year, I\u2019ve been able to work on a lot of really cool things. In this post, I\u2019ll review a subset of the more interesting and visual stuff I\u2019ve built. If you\u2019re into terminals and command line tooling, you\u2019ll hopefully see at least one thing of interest!

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#a-file-manager-powered-by-textual","title":"A file manager powered by Textual","text":"

    I\u2019ve been slowly developing a file manager as a \u201cdogfooding\u201d project for Textual. It takes inspiration from tools such as Ranger and Midnight Commander.

    As of December 2022, it lets you browse your file system, filtering, multi-selection, creating and deleting files/directories, opening files in your $EDITOR and more.

    I\u2019m happy with how far this project has come \u2014 I think it\u2019s a good example of the type of powerful application that can be built with Textual with relatively little code. I\u2019ve been able to focus on features, instead of worrying about terminal emulator implementation details.

    The project is available on GitHub.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#better-diffs-in-the-terminal","title":"Better diffs in the terminal","text":"

    Diffs in the terminal are often difficult to read at a glance. I wanted to see how close I could get to achieving a diff display of a quality similar to that found in the GitHub UI.

    To attempt this, I built a tool called Dunk. It\u2019s a command line program which you can pipe your git diff output into, and it\u2019ll convert it into something which I find much more readable.

    Although I\u2019m not particularly proud of the code - there are a lot of \u201chacks\u201d going on, but I\u2019m proud of the result. If anything, it shows what can be achieved for tools like this.

    For many diffs, the difference between running git diff and git diff | dunk | less -R is night and day.

    It\u2019d be interesting to revisit this at some point. It has its issues, but I\u2019d love to see how it can be used alongside Textual to build a terminal-based diff/merge tool. Perhaps it could be combined with\u2026

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#code-editor-floating-gutter","title":"Code editor floating gutter","text":"

    This is a common feature in text editors and IDEs: when you scroll to the right, you should still be able to see what line you\u2019re on. Out of interest, I tried to recreate the effect in the terminal using Textual.

    Textual CSS offers a dock property which allows you to attach a widget to an edge of its parent. By creating a widget that contains a vertical list of numbers and setting the dock property to left, we can create a floating gutter effect. Then, we just need to keep the scroll_y in sync between the gutter and the content to ensure the line numbers stay aligned.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#dropdown-autocompletion-menu","title":"Dropdown autocompletion menu","text":"

    While working on Shira (a proof-of-concept, terminal-based Python object explorer), I wrote some autocompleting dropdown functionality.

    Textual forgoes the z-index concept from browser CSS and instead uses a \u201cnamed layer\u201d system. Using the layers property you can defined an ordered list of named layers, and using the layer property, you can assign a descendant widget to one of those layers.

    By creating a new layer above all others and assigning a widget to that layer, we can ensure that widget is painted above everything else.

    In order to determine where to place the dropdown, we can track the current value in the dropdown by watching the reactive input \u201cvalue\u201d inside the Input widget. This method will be called every time the value of the Input changes, and we can use this hook to amend the position of our dropdown position to accommodate for the length of the input value.

    I\u2019ve now extracted this into a separate library called textual-autocomplete.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#tabs-with-animated-underline","title":"Tabs with animated underline","text":"

    The aim here was to create a tab widget with underlines that animates smoothly as another tab is selected.

    The difficulty with implementing something like this is that we don\u2019t have pixel-perfect resolution when animating - a terminal window is just a big grid of fixed-width character cells.

    However, when animating things in a terminal, we can often achieve better granularity using Unicode related tricks. In this case, instead of shifting the bar along one whole cell, we adjust the endings of the bar to be a character which takes up half of a cell.

    The exact characters that form the bar are \"\u257a\", \"\u2501\" and \"\u2578\". When the bar sits perfectly within cell boundaries, every character is \u201c\u2501\u201d. As it travels over a cell boundary, the left and right ends of the bar are updated to \"\u257a\" and \"\u2578\" respectively.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#snapshot-testing-for-terminal-apps","title":"Snapshot testing for terminal apps","text":"

    One of the great features we added to Rich this year was the ability to export console contents to an SVG. This feature was later exposed to Textual, allowing users to capture screenshots of their running Textual apps. Ultimately, I ended up creating a tool for snapshot testing in the Textual codebase.

    Snapshot testing is used to ensure that Textual output doesn\u2019t unexpectedly change. On disk, we store what we expect the output to look like. Then, when we run our unit tests, we get immediately alerted if the output has changed.

    This essentially automates the process of manually spinning up several apps and inspecting them for unexpected visual changes. It\u2019s great for catching subtle regressions!

    In Textual, each CSS property has its own canonical example and an associated snapshot test. If we accidentally break a property in a way that affects the visual output, the chances of it sneaking into a release are greatly reduced, because the corresponding snapshot test will fail.

    As part of this work, I built a web interface for comparing snapshots with test output. There\u2019s even a little toggle which highlights the differences, since they\u2019re sometimes rather subtle.

    Since the terminal output shown in the video above is just an SVG image, I was able to add the \"Show difference\" functionality by overlaying the two images and applying a single CSS property: mix-blend-mode: difference;.

    The snapshot testing functionality itself is implemented as a pytest plugin, and it builds on top of a snapshot testing framework called syrupy.

    It's quite likely that this will eventually be exposed to end-users of Textual.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#demonstrating-animation","title":"Demonstrating animation","text":"

    I built an example app to demonstrate how to animate in Textual and the available easing functions.

    The smoothness here is achieved using tricks similar to those used in the tabs I discussed earlier. In fact, the bar that animates in the video above is the same Rich renderable that is used by Textual's scrollbars.

    You can play with this app by running textual easing. Please use animation sparingly.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#developer-console","title":"Developer console","text":"

    When developing terminal based applications, performing simple debugging using print can be difficult, since the terminal is in application mode.

    A project I worked on earlier in the year to improve the situation was the Textual developer console, which you can launch with textual console.

    On the right, Dave's 5x5 Textual app. On the left, the Textual console.

    Then, by running a Textual application with the --dev flag, all standard output will be redirected to it. This means you can use the builtin print function and still immediately see the output. Textual itself also writes information to this console, giving insight into the messages that are flowing through an application.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#pixel-art","title":"Pixel art","text":"

    Cells in the terminal are roughly two times taller than they are wide. This means, that two horizontally adjacent cells form an approximate square.

    Using this fact, I wrote a simple library based on Rich and PIL which can convert an image file into terminal output. You can find the library, rich-pixels, on GitHub.

    It\u2019s particularly good for displaying simple pixel art images. The SVG image below is also a good example of the SVG export functionality I touched on earlier.

    Rich

    Since the library generates an object which is renderable using Rich, these can easily be embedded inside Textual applications.

    Here's an example of that in a scrapped \"Pok\u00e9dex\" app I threw together:

    This is a rather naive approach to the problem... but I did it for fun!

    Other methods for displaying images in the terminal include:

    • A more advanced library like chafa, which uses a range of Unicode characters to achieve a more accurate representation of the image.
    • One of the available terminal image protocols, such as Sixel, Kitty\u2019s Terminal Graphics Protocol, and iTerm Inline Images Protocol.

    That was a whirlwind tour of just some of the projects I tackled in 2022. If you found it interesting, be sure to follow me on Twitter. I don't post often, but when I do, it's usually about things similar to those I've discussed here.

    "},{"location":"blog/2022/11/06/new-blog/","title":"New Blog","text":"

    Welcome to the first post on the Textual blog.

    I plan on using this as a place to make announcements regarding new releases of Textual, and any other relevant news.

    The first piece of news is that we've reorganized this site a little. The Events, Styles, and Widgets references are now under \"Reference\", and what used to be under \"Reference\" is now \"API\" which contains API-level documentation. I hope that's a little clearer than it used to be!

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/","title":"So you're looking for a wee bit of Textual help...","text":""},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#introduction","title":"Introduction","text":"

    Quote

    Patience, Highlander. You have done well. But it'll take time. You are generations being born and dying. You are at one with all living things. Each man's thoughts and dreams are yours to know. You have power beyond imagination. Use it well, my friend. Don't lose your head.

    Juan S\u00e1nchez Villalobos Ram\u00edrez, Chief metallurgist to King Charles V of Spain

    As of the time of writing, I'm a couple or so days off having been with Textualize for 3 months. It's been fun, and educational, and every bit as engaging as I'd hoped, and more. One thing I hadn't quite prepared for though, but which I really love, is how so many other people are learning Textual along with me.

    Even in those three months the library has changed and expanded quite a lot, and it continues to do so. Meanwhile, more people are turning up and using the framework; you can see this online in social media, blogs and of course in the ever-growing list of projects on GitHub which depend on Textual.

    This inevitably means there's a lot of people getting to grips with a new tool, and one that is still a bit of a moving target. This in turn means lots of people are coming to us to get help.

    As I've watched this happen I've noticed a few patterns emerging. Some of these good or neutral, some... let's just say not really beneficial to those seeking the help, or to those trying to provide the help. So I wanted to write a little bit about the different ways you can get help with Textual and your Textual-based projects, and to also try and encourage people to take the most helpful and positive approach to getting that help.

    Now, before I go on, I want to make something very clear: I'm writing this as an individual. This is my own personal view, and my own advice from me to anyone who wishes to take it. It's not Textual (the project) or Textualize (the company) policy, rules or guidelines. This is just some ageing hacker's take on how best to go about asking for help, informed by years of asking for and also providing help in email, on Usenet, on forums, etc.

    Or, put another way: if what you read in here seems sensible to you, I figure we'll likely have already hit it off over on GitHub or in the Discord server. ;-)

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#where-to-go-for-help","title":"Where to go for help","text":"

    At this point this is almost a bit of an FAQ itself, so I thought I'd address it here: where's the best place to ask for help about Textual, and what's the difference between GitHub Issues, Discussions and our Discord server?

    I'd suggest thinking of them like this:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#discord","title":"Discord","text":"

    You have a question, or need help with something, and perhaps you could do with a reply as soon as possible. But, and this is the really important part, it doesn't matter if you don't get a response. If you're in this situation then the Discord server is possibly a good place to start. If you're lucky someone will be hanging about who can help out.

    I can't speak for anyone else, but keep this in mind: when I look in on Discord I tend not to go scrolling back much to see if anything has been missed. If something catches my eye, I'll try and reply, but if it doesn't... well, it's mostly an instant chat thing so I don't dive too deeply back in time.

    Going from Discord to a GitHub issue

    As a slight aside here: sometimes people will pop up in Discord, ask a question about something that turns out looking like a bug, and that's the last we hear of it. Please, please, please, if this happens, the most helpful thing you can do is go raise an issue for us. It'll help us to keep track of problems, it'll help get your problem fixed, it'll mean everyone benefits.

    My own advice would be to treat Discord as an ephemeral resource. It happens in the moment but fades away pretty quickly. It's like knocking on a friend's door to see if they're in. If they're not in, you might leave them a note, which is sort of like going to...

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#github","title":"GitHub","text":"

    On the other hand, if you have a question or need some help or something where you want to stand a good chance of the Textual developers (amongst others) seeing it and responding, I'd recommend that GitHub is the place to go. Dropping something into the discussions there, or leaving an issue, ensures it'll get seen. It won't get lost.

    As for which you should use -- a discussion or an issue -- I'd suggest this: if you need help with something, or you want to check your understanding of something, or you just want to be sure something is a problem before taking it further, a discussion might be the best thing. On the other hand, if you've got a clear bug or feature request on your hands, an issue makes a lot of sense.

    Don't worry if you're not sure which camp your question or whatever falls into though; go with what you think is right. There's no harm done either way (I may move an issue to a discussion first before replying, if it's really just a request for help -- but that's mostly so everyone can benefit from finding it in the right place later on down the line).

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#the-dos-and-donts-of-getting-help","title":"The dos and don'ts of getting help","text":"

    Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A little bit. Again, please remember, this isn't a set of rules, this isn't a set of official guidelines, this is just a bunch of \"if you want my advice, and I know you didn't ask but you've read this far so you actually sort of did don't say I didn't warn you!\" waffle.

    This isn't going to be an exhaustive collection, far from it. But I feel these are some important highlights.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#do","title":"Do...","text":"

    When looking for help, in any of the locations mentioned above, I'd totally encourage:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-clear-and-detailed","title":"Be clear and detailed","text":"

    Too much detail is almost always way better than not enough. \"My program didn't run\", often even with some of the code supplied, is so much harder to help than \"I ran this code I'm posting here, and I expected this particular outcome, and I expected it because I'd read this particular thing in the docs and had comprehended it to mean this, but instead the outcome was this exception here, and I'm a bit stuck -- can someone offer some pointers?\"

    The former approach means there often ends up having to be a back and forth which can last a long time, and which can sometimes be frustrating for the person asking. Manage frustration: be clear, tell us everything you can.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#say-what-resources-youve-used-already","title":"Say what resources you've used already","text":"

    If you've read the potions of the documentation that relate to what you're trying to do, it's going to be really helpful if you say so. If you don't, it might be assumed you haven't and you may end up being pointed at them.

    So, please, if you've checked the documentation, looked in the FAQ, done a search of past issues or discussions or perhaps even done a search on the Discord server... please say so.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-polite","title":"Be polite","text":"

    This one can go a long way when looking for help. Look, I get it, programming is bloody frustrating at times. We've all rage-quit some code at some point, I'm sure. It's likely going to be your moment of greatest frustration when you go looking for help. But if you turn up looking for help acting all grumpy and stuff it's not going to come over well. Folk are less likely to be motivated to lend a hand to someone who seems rather annoyed.

    If you throw in a please and thank-you here and there that makes it all the better.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#fully-consider-the-replies","title":"Fully consider the replies","text":"

    You could find yourself getting a reply that you're sure won't help at all. That's fair. But be sure to fully consider it first. Perhaps you missed the obvious along the way and this is 100% the course correction you'd unknowingly come looking for in the first place. Sure, the person replying might have totally misunderstood what was being asked, or might be giving a wrong answer (it me! I've totally done that and will again!), but even then a reply along the lines of \"I'm not sure that's what I'm looking for, because...\" gets everyone to the solution faster than \"lol nah\".

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#entertain-what-might-seem-like-odd-questions","title":"Entertain what might seem like odd questions","text":"

    Aye, I get it, being asked questions when you're looking for an answer can be a bit frustrating. But if you find yourself on the receiving end of a small series of questions about your question, keep this in mind: Textual is still rather new and still developing and it's possible that what you're trying to do isn't the correct way to do that thing. To the person looking to help you it may seem to them you have an XY problem.

    Entertaining those questions might just get you to the real solution to your problem.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#allow-for-language-differences","title":"Allow for language differences","text":"

    You don't need me to tell you that a project such as Textual has a global audience. With that rather obvious fact comes the other fact that we don't all share the same first language. So, please, as much as possible, try and allow for that. If someone is trying to help you out, and they make it clear they're struggling to follow you, keep this in mind.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#acknowledge-the-answer","title":"Acknowledge the answer","text":"

    I suppose this is a variation on \"be polite\" (really, a thanks can go a long way), but there's more to this than a friendly acknowledgement. If someone has gone to the trouble of offering some help, it's helpful to everyone who comes after you to acknowledge if it worked or not. That way a future help-seeker will know if the answer they're reading stands a chance of being the right one.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#accept-that-textual-is-zero-point-software-right-now","title":"Accept that Textual is zero-point software (right now)","text":"

    Of course the aim is to have every release of Textual be stable and useful, but things will break. So, please, do keep in mind things like:

    • Textual likely doesn't have your feature of choice just yet.
    • We might accidentally break something (perhaps pinning Textual and testing each release is a good plan here?).
    • We might deliberately break something because we've decided to take a particular feature or way of doing things in a better direction.

    Of course it can be a bit frustrating a times, but overall the aim is to have the best framework possible in the long run.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#dont","title":"Don't...","text":"

    Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd personally discourage:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#lack-patience","title":"Lack patience","text":"

    Sure, it can be annoying. You're in your flow, you've got a neat idea for a thing you want to build, you're stuck on one particular thing and you really need help right now! Thing is, that's unlikely to happen. Badgering individuals, or a whole resource, to reply right now, or complaining that it's been $TIME_PERIOD since you asked and nobody has replied... that's just going to make people less likely to reply.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#unnecessarily-tag-individuals","title":"Unnecessarily tag individuals","text":"

    This one often goes hand in hand with the \"lack patience\" thing: Be it asking on Discord, or in GitHub issues, discussions or even PRs, unnecessarily tagging individuals is a bit rude. Speaking for myself and only myself: I love helping folk with Textual. If I could help everyone all the time the moment they have a problem, I would. But it doesn't work like that. There's any number of reasons I might not be responding to a particular request, including but not limited to (here I'm talking personally because I don't want to speak for anyone else, but I'm sure I'm not alone here):

    • I have a job. Sure, my job is (in part) Textual, but there's more to it than that particular issue. I might be doing other stuff.
    • I have my own projects to work on too. I like coding for fun as well (or writing preaching old dude blog posts like this I guess, but you get the idea).
    • I actually have other interests outside of work hours so I might actually be out doing a 10k in the local glen, or battling headcrabs in VR, or something.
    • Housework. :-/

    You get the idea though. So while I'm off having a well-rounded life, it's not good to get unnecessarily intrusive alerts to something that either a) doesn't actually directly involve me or b) could wait.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#seek-personal-support","title":"Seek personal support","text":"

    Again, I'm going to speak totally for myself here, but I also feel the general case is polite for all: there's a lot of good support resources available already; sending DMs on Discord or Twitter or in the Fediverse, looking for direct personal support, isn't really the best way to get help. Using the public/collective resources is absolutely the best way to get that help. Why's it a bad idea to dive into DMs? Here's some reasons I think it's not a good idea:

    • It's a variation on \"unnecessarily tagging individuals\".
    • You're short-changing yourself when it comes to getting help. If you ask somewhere more public you're asking a much bigger audience, who collectively have more time, more knowledge and more experience than a single individual.
    • Following on from that, any answers can be (politely) fact-checked or enhanced by that audience, resulting in a better chance of getting the best help possible.
    • The next seeker-of-help gets to miss out on your question and the answer. If asked and answered in public, it's a record that can help someone else in the future.
    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#doubt-your-ability-or-skill-level","title":"Doubt your ability or skill level","text":"

    I suppose this should really be phrased as a do rather than a don't, as here I want to encourage something positive. A few times I've helped people out who have been very apologetic about their questions being \"noob\" questions, or about how they're fairly new to Python, or programming in general. Really, please, don't feel the need to apologise and don't be ashamed of where you're at.

    If you've asked something that's obviously answered in the documentation, that's not a problem; you'll likely get pointed at the docs and it's what happens next that's the key bit. If the attitude is \"oh, cool, that's exactly what I needed to be reading, thanks!\" that's a really positive thing. The only time it's a problem is when there's a real reluctance to use the available resources. We've all seen that person somewhere at some point, right? ;-)

    Not knowing things is totally cool.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#conclusion","title":"Conclusion","text":"

    So, that's my waffle over. As I said at the start: this is my own personal thoughts on how to get help with Textual, both as someone whose job it is to work on Textual and help people with Textual, and also as a FOSS advocate and supporter who can normally be found helping Textual users when he's not \"on the clock\" too.

    What I've written here isn't exhaustive. Neither is it novel. Plenty has been written on the general subject in the past, and I'm sure more will be written on the subject in the future. I do, however, feel that these are the most common things I notice. I'd say those dos and don'ts cover 90% of \"can I get some help?\" interactions; perhaps closer to 99%.

    Finally, and I think this is the most important thing to remember, the next time you are battling some issue while working with Textual: don't lose your head!

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/","title":"On dog food, the (original) Metaverse, and (not) being bored","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#introduction","title":"Introduction","text":"

    Quote

    Cutler, armed with a schedule, was urging the team to \"eat its own dog food\". Part macho stunt and part common sense, the \"dog food diet\" was the cornerstone of Cutler\u2019s philosophy.

    G. Pascal Zachary \u2014 Show-Stopper!

    I can't remember exactly when it was -- it was likely late in 1994 or some time in 1995 -- when I first came across the concept of, or rather the name for the concept of, \"eating your own dog food\". The idea and the name played a huge part in the book Show-Stopper! by G. Pascal Zachary. The idea wasn't new to me of course; I'd been writing code for over a decade by then and plenty of times I'd built things and then used those things to do things, but it was fascinating to a mostly-self-taught 20-something me to be reading this (excellent -- go read it if you care about the history of your craft) book and to see the idea written down and named.

    While Textualize isn't (thankfully -- really, I do recommend reading the book) anything like working on the team building Windows NT, the idea of taking a little time out from working on Textual, and instead work with Textual, makes a lot of sense. It's far too easy to get focused on adding things and improving things and tweaking things while losing sight of the fact that people will want to build with your product.

    So you can imagine how pleased I was when Will announced that he wanted all of us to spend a couple or so weeks building something with Textual. I had, of course, already written one small application with the library, and had plans for another (in part it's how I ended up working here), but I'd yet to really dive in and try and build something more involved.

    Giving it some thought: I wasn't entirely sure what I wanted to build though. I do want to use Textual to build a brand new terminal-based Norton Guide reader (not my first, not by a long way) but I felt that was possibly a bit too niche, and actually could take a bit too long anyway. Maybe not, it remains to be seen.

    Eventually I decided on this approach: try and do a quick prototype of some daft idea each day or each couple of days, do that for a week or so, and then finally try and settle down on something less trivial. This approach should work well in that it'll help introduce me to more of Textual, help try out a few different parts of the library, and also hopefully discover some real pain-points with working with it and highlight a list of issues we should address -- as seen from the perspective of a developer working with the library.

    So, here I am, at the end of week one. What I want to try and do is briefly (yes yes, I know, this introduction is the antithesis of brief) talk about what I built and perhaps try and highlight some lessons learnt, highlight some patterns I think are useful, and generally do an end-of-week version of a TIL. TWIL?

    Yeah. I guess this is a TWIL.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#gridinfo","title":"gridinfo","text":"

    I started the week by digging out a quick hack I'd done a couple of weeks earlier, with a view to cleaning it up. It started out as a fun attempt to do something with Rich Pixels while also making a terminal-based take on slstats.el. I'm actually pleased with the result and how quickly it came together.

    The point of the application itself is to show some general information about the current state of the Second Life grid (hello to any fellow residents of the original Metaverse!), and to also provide a simple region lookup screen that, using Rich Pixels, will display the object map (albeit in pretty low resolution -- but that's the fun of this!).

    So the opening screen looks like this:

    and a lookup of a region looks like this:

    Here's a wee video of the whole thing in action:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight","title":"Worth a highlight","text":"

    Here's a couple of things from the code that I think are worth a highlight, as things to consider when building Textual apps:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-use-the-default-screen","title":"Don't use the default screen","text":"

    Use of the default Screen that's provided by the App is handy enough, but I feel any non-trivial application should really put as much code as possible in screens that relate to key \"work\". Here's the entirety of my application code:

    class GridInfo( App[ None ] ):\n    \"\"\"TUI app for showing information about the Second Life grid.\"\"\"\n\n    CSS_PATH = \"gridinfo.css\"\n    \"\"\"The name of the CSS file for the app.\"\"\"\n\n    TITLE = \"Grid Information\"\n    \"\"\"str: The title of the application.\"\"\"\n\n    SCREENS = {\n        \"main\": Main,\n        \"region\": RegionInfo\n    }\n    \"\"\"The collection of application screens.\"\"\"\n\n    def on_mount( self ) -> None:\n        \"\"\"Set up the application on startup.\"\"\"\n        self.push_screen( \"main\" )\n

    You'll notice there's no work done in the app, other than to declare the screens, and to set the main screen running when the app is mounted.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-work-hard-on_mount","title":"Don't work hard on_mount","text":"

    My initial version of the application had it loading up the data from the Second Life and GridSurvey APIs in Main.on_mount. This obviously wasn't a great idea as it made the startup appear slow. That's when I realised just how handy call_after_refresh is. This meant I could show some placeholder information and then fire off the requests (3 of them: one to get the main grid information, one to get the grid concurrency data, and one to get the grid size data), keeping the application looking active and updating the display when the replies came in.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points","title":"Pain points","text":"

    While building this app I think there was only really the one pain-point, and I suspect it's mostly more on me than on Textual itself: getting a good layout and playing whack-a-mole with CSS. I suspect this is going to be down to getting more and more familiar with CSS and the terminal (which is different from laying things out for the web), while also practising with various layout schemes -- which is where the revamped Placeholder class is going to be really useful.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#unbored","title":"unbored","text":"

    The next application was initially going to be a very quick hack, but actually turned into a less-trivial build than I'd initially envisaged; not in a negative way though. The more I played with it the more I explored and I feel that this ended up being my first really good exploration of some useful (personal -- your kilometerage may vary) patterns and approaches when working with Textual.

    The application itself is a terminal client for the Bored-API. I had initially intended to roll my own code for working with the API, but I noticed that someone had done a nice library for it and it seemed silly to not build on that. Not needing to faff with that, I could concentrate on the application itself.

    At first I was just going to let the user click away at a button that showed a random activity, but this quickly morphed into a \"why don't I make this into a sort of TODO list builder app, where you can add things to do when you are bored, and delete things you don't care for or have done\" approach.

    Here's a view of the main screen:

    and here's a view of the filter pop-over:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight_1","title":"Worth a highlight","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-put-all-your-bindings-in-one-place","title":"Don't put all your BINDINGS in one place","text":"

    This came about from me overloading the use of the escape key. I wanted it to work more or less like this:

    • If you're inside an activity, move focus up to the activity type selection buttons.
    • If the filter pop-over is visible, close that.
    • Otherwise exit the application.

    It was easy enough to do, and I had an action in the Main screen that escape was bound to (again, in the Main screen) that did all this logic with some if/elif work but it didn't feel elegant. Moreover, it meant that the Footer always displayed the same description for the key.

    That's when I realised that it made way more sense to have a Binding for escape in every widget that was the actual context for escape's use. So I went from one top-level binding to...

    ...\n\nclass Activity( Widget ):\n    \"\"\"A widget that holds and displays a suggested activity.\"\"\"\n\n    BINDINGS = [\n        ...\n        Binding( \"escape\", \"deselect\", \"Switch to Types\" )\n    ]\n\n...\n\nclass Filters( Vertical ):\n    \"\"\"Filtering sidebar.\"\"\"\n\n    BINDINGS = [\n        Binding( \"escape\", \"close\", \"Close Filters\" )\n    ]\n\n...\n\nclass Main( Screen ):\n    \"\"\"The main application screen.\"\"\"\n\n    BINDINGS = [\n        Binding( \"escape\", \"quit\", \"Close\" )\n    ]\n    \"\"\"The bindings for the main screen.\"\"\"\n

    This was so much cleaner and I got better Footer descriptions too. I'm going to be leaning hard on this approach from now on.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#messages-are-awesome","title":"Messages are awesome","text":"

    Until I wrote this application I hadn't really had a need to define or use my own Messages. During work on this I realised how handy they really are. In the code I have an Activity widget which takes care of the job of moving itself amongst its siblings if the user asks to move an activity up or down. When this happens I also want the Main screen to save the activities to the filesystem as things have changed.

    Thing is: I don't want the screen to know what an Activity is capable of and I don't want an Activity to know what the screen is capable of; especially the latter as I really don't want a child of a screen to know what the screen can do (in this case \"save stuff\").

    This is where messages come in. Using a message I could just set things up so that the Activity could shout out \"HEY I JUST DID A THING THAT CHANGES ME\" and not care who is listening and not care what they do with that information.

    So, thanks to this bit of code in my Activity widget...

        class Moved( Message ):\n        \"\"\"A message to indicate that an activity has moved.\"\"\"\n\n    def action_move_up( self ) -> None:\n        \"\"\"Move this activity up one place in the list.\"\"\"\n        if self.parent is not None and not self.is_first:\n            parent = cast( Widget, self.parent )\n            parent.move_child(\n                self, before=parent.children.index( self ) - 1\n            )\n            self.emit_no_wait( self.Moved( self ) )\n            self.scroll_visible( top=True )\n

    ...the Main screen can do this:

        def on_activity_moved( self, _: Activity.Moved ) -> None:\n        \"\"\"React to an activity being moved.\"\"\"\n        self.save_activity_list()\n

    Warning

    The code above used emit_no_wait. Since this blog post was first published that method has been removed from Textual. You should use post_message_no_wait or post_message instead now.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points_1","title":"Pain points","text":"

    On top of the issues of getting to know terminal-based-CSS that I mentioned earlier:

    • Textual currently lacks any sort of selection list or radio-set widget. This meant that I couldn't quite do the activity type picking how I would have wanted. Of course I could have rolled my own widgets for this, but I think I'd sooner wait until such things are in Textual itself.
    • Similar to that, I could have used some validating Input widgets. They too are on the roadmap but I managed to cobble together fairly good working versions for my purposes. In doing so though I did further highlight that the reactive attribute facility needs a wee bit more attention as I ran into some (already-known) bugs. Thankfully in my case it was a very easy workaround.
    • Scrolling in general seems a wee bit off when it comes to widgets that are more than one line tall. While there's nothing really obvious I can point my finger at, I'm finding that scrolling containers sometimes get confused about what should be in view. This becomes very obvious when forcing things to scroll from code. I feel this deserves a dedicated test application to explore this more.
    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#conclusion","title":"Conclusion","text":"

    The first week of \"dogfooding\" has been fun and I'm more convinced than ever that it's an excellent exercise for Textualize to engage in. I didn't quite manage my plan of \"one silly trivial prototype per day\", which means I've ended up with two (well technically one and a half I guess given that gridinfo already existed as a prototype) applications rather than four. I'm okay with that. I got a lot of utility out of this.

    Now to look at the list of ideas I have going and think about what I'll kick next week off with...

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/","title":"What I learned from my first non-trivial PR","text":"PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

    It's 8:59 am and, by my Portuguese standards, it is freezing cold outside: 5 or 6 degrees Celsius. It is my second day at Textualize and I just got into the office. I undress my many layers of clothing to protect me from the Scottish cold and I sit down in my improvised corner of the Textualize office. As I sit down, I turn myself in my chair to face my boss and colleagues to ask \u201cSo, what should I do today?\u201d. I was not expecting Will's answer, but the challenge excited me:

    \u201cI thought I'll just throw you in the deep end and have you write some code.\u201d

    What happened next was that I spent two days working on PR #1229 to add a new widget to the Textual code base. At the time of writing, the pull request has not been merged yet. Well, to be honest with you, it hasn't even been reviewed by anyone... But that won't stop me from blogging about some of the things I learned while creating this PR.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#the-placeholder-widget","title":"The placeholder widget","text":"

    This PR adds a widget called Placeholder to Textual. As per the documentation, this widget \u201cis meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.\u201d

    The point of the placeholder widget is that you can focus on building the layout of your app without having to have all of your (custom) widgets ready. The placeholder widget also displays a couple of useful pieces of information to help you work out the layout of your app, namely the ID of the widget itself (or a custom label, if you provide one) and the width and height of the widget.

    As an example of usage of the placeholder widget, you can refer to the screenshot at the top of this blog post, which I included below so you don't have to scroll up:

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

    The top left and top right widgets have custom labels. Immediately under the top right placeholder, you can see some placeholders identified as #p3, #p4, and #p5. Those are the IDs of the respective placeholders. Then, rows 2 and 3 contain some placeholders that show their respective size and some placeholders that just contain some text.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#bootstrapping-the-code-for-the-widget","title":"Bootstrapping the code for the widget","text":"

    So, how does a code monkey start working on a non-trivial PR within 24 hours of joining a company? The answer is simple: just copy and paste code! But instead of copying and pasting from Stack Overflow, I decided to copy and paste from the internal code base.

    My task was to create a new widget, so I thought it would be a good idea to take a look at the implementation of other Textual widgets. For some reason I cannot seem to recall, I decided to take a look at the implementation of the button widget that you can find in _button.py. By looking at how the button widget is implemented, I could immediately learn a few useful things about what I needed to do and some other things about how Textual works.

    For example, a widget can have a class attribute called DEFAULT_CSS that specifies the default CSS for that widget. I learned this just from staring at the code for the button widget.

    Studying the code base will also reveal the standards that are in place. For example, I learned that for a widget with variants (like the button with its \u201csuccess\u201d and \u201cerror\u201d variants), the widget gets a CSS class with the name of the variant prefixed by a dash. You can learn this by looking at the method Button.watch_variant:

    class Button(Static, can_focus=True):\n    # ...\n\n    def watch_variant(self, old_variant: str, variant: str):\n        self.remove_class(f\"-{old_variant}\")\n        self.add_class(f\"-{variant}\")\n

    In short, looking at code and files that are related to the things you need to do is a great way to get information about things you didn't even know you needed.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#handling-the-placeholder-variant","title":"Handling the placeholder variant","text":"

    A button widget can have a different variant, which is mostly used by Textual to determine the CSS that should apply to the given button. For the placeholder widget, we want the variant to determine what information the placeholder shows. The original GitHub issue mentions 5 variants for the placeholder:

    • a variant that just shows a label or the placeholder ID;
    • a variant that shows the size and location of the placeholder;
    • a variant that shows the state of the placeholder (does it have focus? is the mouse over it?);
    • a variant that shows the CSS that is applied to the placeholder itself; and
    • a variant that shows some text inside the placeholder.

    The variant can be assigned when the placeholder is first instantiated, for example, Placeholder(\"css\") would create a placeholder that shows its own CSS. However, we also want to have an on_click handler that cycles through all the possible variants. I was getting ready to reinvent the wheel when I remembered that the standard module itertools has a lovely tool that does exactly what I needed! Thus, all I needed to do was create a new cycle through the variants each time a placeholder is created and then grab the next variant whenever the placeholder is clicked:

    class Placeholder(Static):\n    def __init__(\n        self,\n        variant: PlaceholderVariant = \"default\",\n        *,\n        label: str | None = None,\n        name: str | None = None,\n        id: str | None = None,\n        classes: str | None = None,\n    ) -> None:\n        # ...\n\n        self.variant = self.validate_variant(variant)\n        # Set a cycle through the variants with the correct starting point.\n        self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\n        while next(self._variants_cycle) != self.variant:\n            pass\n\n    def on_click(self) -> None:\n        \"\"\"Click handler to cycle through the placeholder variants.\"\"\"\n        self.cycle_variant()\n\n    def cycle_variant(self) -> None:\n        \"\"\"Get the next variant in the cycle.\"\"\"\n        self.variant = next(self._variants_cycle)\n

    I am just happy that I had the insight to add this little while loop when a placeholder is instantiated:

    from itertools import cycle\n# ...\nclass Placeholder(Static):\n    # ...\n    def __init__(...):\n        # ...\n        self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\n        while next(self._variants_cycle) != self.variant:\n            pass\n

    Can you see what would be wrong if this loop wasn't there?

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#updating-the-render-of-the-placeholder-on-variant-change","title":"Updating the render of the placeholder on variant change","text":"

    If the variant of the placeholder is supposed to determine what information the placeholder shows, then that information must be updated every time the variant of the placeholder changes. Thankfully, Textual has reactive attributes and watcher methods, so all I needed to do was... Defer the problem to another method:

    class Placeholder(Static):\n    # ...\n    variant = reactive(\"default\")\n    # ...\n    def watch_variant(\n        self, old_variant: PlaceholderVariant, variant: PlaceholderVariant\n    ) -> None:\n        self.validate_variant(variant)\n        self.remove_class(f\"-{old_variant}\")\n        self.add_class(f\"-{variant}\")\n        self.call_variant_update()  # <-- let this method do the heavy lifting!\n

    Doing this properly required some thinking. Not that the current proposed solution is the best possible, but I did think of worse alternatives while I was thinking how to tackle this. I wasn't entirely sure how I would manage the variant-dependant rendering because I am not a fan of huge conditional statements that look like switch statements:

    if variant == \"default\":\n    # render the default placeholder\nelif variant == \"size\":\n    # render the placeholder with its size\nelif variant == \"state\":\n    # render the state of the placeholder\nelif variant == \"css\":\n    # render the placeholder with its CSS rules\nelif variant == \"text\":\n    # render the placeholder with some text inside\n

    However, I am a fan of using the built-in getattr and I thought of creating a rendering method for each different variant. Then, all I needed to do was make sure the variant is part of the name of the method so that I can programmatically determine the name of the method that I need to call. This means that the method Placeholder.call_variant_update is just this:

    class Placeholder(Static):\n    # ...\n    def call_variant_update(self) -> None:\n        \"\"\"Calls the appropriate method to update the render of the placeholder.\"\"\"\n        update_variant_method = getattr(self, f\"_update_{self.variant}_variant\")\n        update_variant_method()\n

    If self.variant is, say, \"size\", then update_variant_method refers to _update_size_variant:

    class Placeholder(Static):\n    # ...\n    def _update_size_variant(self) -> None:\n        \"\"\"Update the placeholder with the size of the placeholder.\"\"\"\n        width, height = self.size\n        self._placeholder_label.update(f\"[b]{width} x {height}[/b]\")\n

    This variant \"size\" also interacts with resizing events, so we have to watch out for those:

    class Placeholder(Static):\n    # ...\n    def on_resize(self, event: events.Resize) -> None:\n        \"\"\"Update the placeholder \"size\" variant with the new placeholder size.\"\"\"\n        if self.variant == \"size\":\n            self._update_size_variant()\n
    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#deleting-code-is-a-hurtful-blessing","title":"Deleting code is a (hurtful) blessing","text":"

    To conclude this blog post, let me muse about the fact that the original issue mentioned five placeholder variants and that my PR only includes two and a half.

    After careful consideration and after coming up with the getattr mechanism to update the display of the placeholder according to the active variant, I started showing the \u201cfinal\u201d product to Will and my other colleagues. Eventually, we ended up getting rid of the variant for CSS and the variant that shows the placeholder state. This means that I had to delete part of my code even before it saw the light of day.

    On the one hand, deleting those chunks of code made me a bit sad. After all, I had spent quite some time thinking about how to best implement that functionality! But then, it was time to write documentation and tests, and I verified that the best code is the code that you don't even write! The code you don't write is guaranteed to have zero bugs and it also does not need any documentation whatsoever!

    So, it was a shame that some lines of code I poured my heart and keyboard into did not get merged into the Textual code base. On the other hand, I am quite grateful that I won't have to fix the bugs that will certainly reveal themselves in a couple of weeks or months from now. Heck, the code hasn't been merged yet and just by writing this blog post I noticed a couple of tweaks that were missing!

    "},{"location":"blog/2023/07/29/pull-requests-are-cake-or-puppies/","title":"Pull Requests are cake or puppies","text":"

    Broadly speaking, there are two types of contributions you can make to an Open Source project.

    The first type is typically a bug fix, but could also be a documentation update, linting fix, or other change which doesn't impact core functionality. Such a contribution is like cake. It's a simple, delicious, gift to the project.

    The second type of contribution often comes in the form of a new feature. This contribution likely represents a greater investment of time and effort than a bug fix. It is still a gift to the project, but this contribution is not cake.

    A feature PR has far more in common with a puppy. The maintainer(s) may really like the feature but hesitate to merge all the same. They may even reject the contribution entirely. This is because a feature PR requires an ongoing burden to maintain. In the same way that a puppy needs food and walkies, a new feature will require updates and fixes long after the original contribution. Even if it is an amazing feature, the maintainer may not want to commit to that ongoing work.

    The chances of a feature being merged can depend on the maturity of the project. At the beginning of a project, a maintainer may be delighted with a new feature contribution. After all, having others join you to build something is the joy of Open Source. And yet when a project gets more mature there may be a growing resistance to adding new features, and a greater risk that a feature PR is rejected or sits unappreciated in the PR queue.

    So how should a contributor avoid this? If there is any doubt, it's best to propose the feature to the maintainers before undertaking the work. In all likelihood they will be happy for your contribution, just be prepared for them to say \"thanks but no thanks\". Don't take it as a rejection of your gift: it's just that the maintainer can't commit to taking on a puppy.

    There are other ways to contribute code to a project that don't require the code to be merged in to the core. You could publish your change as a third party library. Take it from me: maintainers love it when their project spawns an ecosystem. You could also blog about how you solved your problem without an update to the core project. Having a resource that can be googled for, or a maintainer can direct people to, can be a huge help.

    What prompted me to think about this is that my two main projects, Rich and Textual, are at quite different stages in their lifetime. Rich is relatively mature, and I'm unlikely to accept a puppy. If you can achieve what you need without adding to the core library, I am probably going to decline a new feature. Textual is younger and still accepting puppies \u2014 in addition to stick insects, gerbils, capybaras and giraffes.

    Tip

    If you are maintainer, and you do have to close a feature PR, feel free to link to this post.

    Join us on the Discord Server if you want to discuss puppies and other creatures.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/","title":"Textual 0.11.0 adds a beautiful Markdown widget","text":"

    We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have we been up to?

    The headline feature of this release is the enhanced Markdown support. Here's a screenshot of an example:

    MarkdownApp \u258bHeader\u00a0level\u00a06\u00a0content. \u25bc\u00a0\u2160\u00a0Textual\u00a0Markdown\u00a0Browser\u00a0-\u00a0Demo\u258b \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Headers\u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2162\u00a0This\u00a0is\u00a0H3\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2163\u00a0This\u00a0is\u00a0H4\u258b\u258eTypography\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2164\u00a0This\u00a0is\u00a0H5\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u2165\u00a0This\u00a0is\u00a0H6\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Typography\u258bThe\u00a0usual\u00a0Markdown\u00a0typography\u00a0is\u00a0supported.\u00a0The\u00a0exact\u00a0output\u00a0depends\u00a0on\u00a0 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Emphasis\u258byour\u00a0terminal,\u00a0although\u00a0most\u00a0are\u00a0fairly\u00a0consistent.\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strong\u258b \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strikethrough\u258bEmphasis \u2502\u00a0\u00a0\u00a0\u2517\u2501\u2501\u00a0\u2162\u00a0Inline\u00a0code\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u2161\u00a0Fences\u258bEmphasis\u00a0is\u00a0rendered\u00a0with\u00a0*asterisks*,\u00a0and\u00a0looks\u00a0like\u00a0this; \u251c\u2500\u2500\u00a0\u2161\u00a0Quote\u258b \u2514\u2500\u2500\u00a0\u2161\u00a0Tables\u258bStrong \u258b\u2594\u2594\u2594\u2594\u2594\u2594 \u258bUse\u00a0two\u00a0asterisks\u00a0to\u00a0indicate\u00a0strong\u00a0which\u00a0renders\u00a0in\u00a0bold,\u00a0e.g.\u00a0 \u258b**strong**\u00a0render\u00a0strong. \u258b \u258bStrikethrough \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bTwo\u00a0tildes\u00a0indicates\u00a0strikethrough,\u00a0e.g.\u00a0~~cross\u00a0out~~\u00a0render\u00a0cross\u00a0out. \u258b\u2582\u2582 \u258bInline\u00a0code \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bInline\u00a0code\u00a0is\u00a0indicated\u00a0by\u00a0backticks.\u00a0e.g.\u00a0import\u00a0this. \u258b \u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258b\u258e\u258b \u258b\u258eFences\u258b \u258b\u258e\u258b \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bFenced\u00a0code\u00a0blocks\u00a0are\u00a0introduced\u00a0with\u00a0three\u00a0back-ticks\u00a0and\u00a0the\u00a0optional\u00a0 \u258bparser.\u00a0Here\u00a0we\u00a0are\u00a0rendering\u00a0the\u00a0code\u00a0in\u00a0a\u00a0sub-widget\u00a0with\u00a0syntax\u00a0 \u258bhighlighting\u00a0and\u00a0indent\u00a0guides. \u258b \u258bIn\u00a0the\u00a0future\u00a0I\u00a0think\u00a0we\u00a0could\u00a0add\u00a0controls\u00a0to\u00a0export\u00a0the\u00a0code,\u00a0copy\u00a0to\u00a0 \u258bthe\u00a0clipboard.\u00a0Heck,\u00a0even\u00a0run\u00a0it\u00a0and\u00a0show\u00a0the\u00a0output? \u258b \u258b \u258b@lru_cache(maxsize=1024) \u258bdefsplit(self,cut_x:int,cut_y:int)->tuple[Region,Region,Regi \u258b\u2502\u00a0\u00a0\u00a0\"\"\"Split\u00a0a\u00a0region\u00a0in\u00a0to\u00a04\u00a0from\u00a0given\u00a0x\u00a0and\u00a0y\u00a0offsets\u00a0(cuts). \u00a0T\u00a0\u00a0TOC\u00a0\u00a0B\u00a0\u00a0Back\u00a0\u00a0F\u00a0\u00a0Forward\u00a0

    Tip

    You can generate these SVG screenshots for your app with textual run my_app.py --screenshot 5 which will export a screenshot after 5 seconds.

    There are actually 2 new widgets: Markdown for a simple Markdown document, and MarkdownViewer which adds browser-like navigation and a table of contents.

    Textual has had support for Markdown since day one by embedding a Rich Markdown object -- which still gives decent results! This new widget adds dynamic controls such as scrollable code fences and tables, in addition to working links.

    In future releases we plan on adding more Markdown extensions, and the ability to easily embed custom widgets within the document. I'm sure there are plenty of interesting applications that could be powered by dynamically generated Markdown documents.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#datatable-improvements","title":"DataTable improvements","text":"

    There has been a lot of work on the DataTable API. We've added the ability to sort the data, which required that we introduce the concept of row and column keys. You can now reference rows / columns / cells by their coordinate or by row / column key.

    Additionally there are new update_cell and update_cell_at methods to update cells after the data has been populated. Future releases will have more methods to manipulate table data, which will make it a very general purpose (and powerful) widget.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#tree-control","title":"Tree control","text":"

    The Tree widget has grown a few methods to programmatically expand, collapse and toggle tree nodes.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#breaking-changes","title":"Breaking changes","text":"

    There are a few breaking changes in this release. These are mostly naming and import related, which should be easy to fix if you are affected. Here's a few notable examples:

    • Checkbox has been renamed to Switch. This is because we plan to introduce complimentary Checkbox and RadioButton widgets in a future release, but we loved the look of Switches too much to drop them.
    • We've dropped the emit and emit_no_wait methods. These methods posted message to the parent widget, but we found that made it problematic to subclass widgets. In almost all situations you want to replace these with self.post_message (or self.post_message_no_wait).

    Be sure to check the CHANGELOG for the full details on potential breaking changes.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#join-us","title":"Join us!","text":"

    We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/","title":"Textual 0.12.0 adds syntactical sugar and batch updates","text":"

    It's been just 9 days since the previous release, but we have a few interesting enhancements to the Textual API to talk about.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#better-compose","title":"Better compose","text":"

    We've added a little syntactical sugar to Textual's compose methods, which aids both readability and editability (that might not be a word).

    First, let's look at the old way of building compose methods. This snippet is taken from the textual colors command.

    for color_name in ColorSystem.COLOR_NAMES:\n\n    items: list[Widget] = [ColorLabel(f'\"{color_name}\"')]\n    for level in LEVELS:\n        color = f\"{color_name}-{level}\" if level else color_name\n        item = ColorItem(\n            ColorBar(f\"${color}\", classes=\"text label\"),\n            ColorBar(\"$text-muted\", classes=\"muted\"),\n            ColorBar(\"$text-disabled\", classes=\"disabled\"),\n            classes=color,\n        )\n        items.append(item)\n\n    yield ColorGroup(*items, id=f\"group-{color_name}\")\n

    This code composes the following color swatches:

    ColorsApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 primary \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b\u2581\u2581 secondary\u258e\"primary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b background\u258e$primary-darken-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b primary-background\u258e$primary-darken-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b secondary-background\u258e$primary-darken-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b surface\u258e$primary$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b panel\u258e$primary-lighten-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b boost\u258e$primary-lighten-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b warning\u258e$primary-lighten-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b error\u258e\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 success \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b accent\u258e\"secondary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u258e\u258b \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Tip

    You can see this by running textual colors from the command line.

    The old way was not all that bad, but it did make it hard to see the structure of your app at-a-glance, and editing compose methods always felt a little laborious.

    Here's the new syntax, which uses context managers to add children to containers:

    for color_name in ColorSystem.COLOR_NAMES:\n    with ColorGroup(id=f\"group-{color_name}\"):\n        yield Label(f'\"{color_name}\"')\n        for level in LEVELS:\n            color = f\"{color_name}-{level}\" if level else color_name\n            with ColorItem(classes=color):\n                yield ColorBar(f\"${color}\", classes=\"text label\")\n                yield ColorBar(\"$text-muted\", classes=\"muted\")\n                yield ColorBar(\"$text-disabled\", classes=\"disabled\")\n

    The context manager approach generally results in fewer lines of code, and presents attributes on the same line as containers themselves. Additionally, adding widgets to a container can be as simple is indenting them.

    You can still construct widgets and containers with positional arguments, but this new syntax is preferred. It's not documented yet, but you can start using it now. We will be updating our examples in the next few weeks.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#batch-updates","title":"Batch updates","text":"

    Textual is smart about performing updates to the screen. When you make a change that might repaint the screen, those changes don't happen immediately. Textual makes a note of them, and repaints the screen a short time later (around a 1/60th of a second). Multiple updates are combined so that Textual does less work overall, and there is none of the flicker you might get with multiple repaints.

    Although this works very well, it is possible to introduce a little flicker if you make changes across multiple widgets. And especially if you add or remove many widgets at once. To combat this we have added a batch_update context manager which tells Textual to disable screen updates until the end of the with block.

    The new Markdown widget uses this context manager when it updates its content. Here's the code:

    with self.app.batch_update():\n    await self.query(\"MarkdownBlock\").remove()\n    await self.mount_all(output)\n

    Without the batch update there are a few frames where the old markdown blocks are removed and the new blocks are added (which would be perceived as a brief flicker). With the update, the update appears instant.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#disabled-widgets","title":"Disabled widgets","text":"

    A few widgets (such as Button) had a disabled attribute which would fade the widget a little and make it unselectable. We've extended this to all widgets. Although it is particularly applicable to input controls, anything may be disabled. Disabling a container makes its children disabled, so you could use this for disabling a form, for example.

    Tip

    Disabled widgets may be styled with the :disabled CSS pseudo-selector.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#preventing-messages","title":"Preventing messages","text":"

    Also in this release is another context manager, which will disable specified Message types. This doesn't come up as a requirement very often, but it can be very useful when it does. This one is documented, see Preventing events for details.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#full-changelog","title":"Full changelog","text":"

    As always see the release page for additional changes and bug fixes.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#join-us","title":"Join us!","text":"

    We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

    "},{"location":"blog/2023/03/09/textual-0140-shakes-up-posting-messages/","title":"Textual 0.14.0 shakes up posting messages","text":"

    Textual version 0.14.0 has landed just a week after 0.13.0.

    Note

    We like fast releases for Textual. Fast releases means quicker feedback, which means better code.

    What's new?

    We did a little shake-up of posting messages which will simplify building widgets. But this does mean a few breaking changes.

    There are two methods in Textual to post messages: post_message and post_message_no_wait. The former was asynchronous (you needed to await it), and the latter was a regular method call. These two methods have been replaced with a single post_message method.

    To upgrade your project to Textual 0.14.0, you will need to do the following:

    • Remove await keywords from any calls to post_message.
    • Replace any calls to post_message_no_wait with post_message.

    Additionally, we've simplified constructing messages classes. Previously all messages required a sender argument, which had to be manually set. This was a clear violation of our \"no boilerplate\" policy, and has been dropped. There is still a sender property on messages / events, but it is set automatically.

    So prior to 0.14.0 you might have posted messages like the following:

    await self.post_message(self.Changed(self, item=self.item))\n

    You can now replace it with this simpler function call:

    self.post_message(self.Change(item=self.item))\n

    This also means that you will need to drop the sender from any custom messages you have created.

    If this was code pre-0.14.0:

    class MyWidget(Widget):\n\n    class Changed(Message):\n        \"\"\"My widget change event.\"\"\"\n        def __init__(self, sender:MessageTarget, item_index:int) -> None:\n            self.item_index = item_index\n            super().__init__(sender)\n

    You would need to make the following change (dropping sender).

    class MyWidget(Widget):\n\n    class Changed(Message):\n        \"\"\"My widget change event.\"\"\"\n        def __init__(self, item_index:int) -> None:\n            self.item_index = item_index\n            super().__init__()\n

    If you have any problems upgrading, join our Discord server, we would be happy to help.

    See the release notes for the full details on this update.

    "},{"location":"blog/2023/03/13/textual-0150-adds-a-tabs-widget/","title":"Textual 0.15.0 adds a tabs widget","text":"

    We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical release cadence of 1 to 2 weeks.

    What's new in this release?

    The highlight of this release is a new Tabs widget to display tabs which can be navigated much like tabs in a browser. Here's a screenshot:

    TabsApp Paul\u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0Halleck \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    In a future release, this will be combined with the ContentSwitcher widget to create a traditional tabbed dialog. Although Tabs is still useful as a standalone widgets.

    Tip

    I like to tweet progress with widgets on Twitter. See the #textualtabs hashtag which documents progress on this widget.

    Also in this release is a new LoadingIndicator widget to display a simple animation while waiting for data. Here's a screenshot:

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    As always, see the release notes for the full details on this update.

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

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/","title":"Textual 0.16.0 adds TabbedContent and border titles","text":"

    Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.

    There are two highlights in this release. In no particular order, the first is TabbedContent which uses a row of tabs to navigate content. You will have likely encountered this UI in the desktop and web. I think in Windows they are known as \"Tabbed Dialogs\".

    This widget combines existing Tabs and ContentSwitcher widgets and adds an expressive interface for composing. Here's a trivial example to use content tabs to navigate a set of three markdown documents:

    def compose(self) -> ComposeResult:\n    with TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\n        yield Markdown(LETO)\n        yield Markdown(JESSICA)\n        yield Markdown(PAUL)\n

    Here's an example of the UI you can create with this widget (note the nesting)!

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#border-titles","title":"Border titles","text":"

    The second highlight is a frequently requested feature (FRF?). Widgets now have the two new string properties, border_title and border_subtitle, which will be displayed within the widget's border.

    You can set the alignment of these titles via border-title-align and border-subtitle-align. Titles may contain Console Markup, so you can add additional color and style to the labels.

    Here's an example of a widget with a title:

    BorderApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 ascii \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 none \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550double\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 hidden\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0must\u00a0not\u00a0fear.\u2551 blank\u2551Fear\u00a0is\u00a0the\u00a0mind-killer.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2551 round\u2551I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551path.\u2551 solid\u2586\u2586\u2551Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551remain.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551\u2551 double\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 dashed \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 heavy \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BTW the above is a command you can run to see the various border styles you can apply to widgets.

    textual borders\n
    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#container-changes","title":"Container changes","text":"

    Breaking change

    If you have an app that uses any container classes, you should read this section.

    We've made a change to containers in this release. Previously all containers had auto scrollbars, which means that any container would scroll if its children didn't fit. With nested layouts, it could be tricky to understand exactly which containers were scrolling. In 0.16.0 we split containers in to scrolling and non-scrolling versions. So Horizontal will now not scroll by default, but HorizontalScroll will have automatic scrollbars.

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#what-else","title":"What else?","text":"

    As always, see the release notes for the full details on this update.

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

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/","title":"Textual 0.17.0 adds translucent screens and Option List","text":"

    This is a surprisingly large release, given it has been just 7 days since the last version (and we were down a developer for most of that time).

    What's new in this release?

    There are two new notable features I want to cover. The first is a compositor effect.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#translucent-screens","title":"Translucent screens","text":"

    Textual has a concept of \"screens\" which you can think of as independent UI modes, each with their own user interface and logic. The App class keeps a stack of these screens so you can switch to a new screen and later return to the previous screen.

    Screens

    See the guide to learn more about the screens API.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

    Screens can be used to build modal dialogs by pushing a screen with controls / buttons, and popping the screen when the user has finished with it. The problem with this approach is that there was nothing to indicate to the user that the original screen was still there, and could be returned to.

    In this release we have added alpha support to the Screen's background color which allows the screen underneath to show through, typically blended with a little color. Applying this to a screen makes it clear than the user can return to the previous screen when they have finished interacting with the modal.

    Here's how you can enable this effect with CSS:

    DialogScreen {\n    align: center middle;\n    background: $primary 30%;\n}\n

    Setting the background to $primary will make the background blue (with the default theme). The addition of 30% sets the alpha so that it will be blended with the background. Here's the kind of effect this creates:

    DialogApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588Good\u00a0for\u00a0natural\u00a0breaks\u00a0in\u00a0the\u00a0content,\u00a0that\u00a0don't\u00a0require\u00a0another\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588header.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258e\u258b\u2588\u258b\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u258eLists\u258b\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u258e\u258b\u2582\u2582\u2588\u258b\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2582\u2582\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u00a01.\u00a0Lists\u00a0can\u00a0be\u00a0ordered\u2588down\u00a0widgets.\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u00a02.\u00a0Lists\u00a0can\u00a0be\u00a0unordered\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u25cf\u00a0I\u00a0must\u00a0not\u00a0fear.\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25aa\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u2584\u2584\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u2023\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2588\u258b\u2588\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2022\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258b\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2b51\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u25aa\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588see\u00a0its\u00a0path.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25cf\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2588\u2588\u2582\u2582\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588remain.\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588Longer\u00a0list\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u00a0\u00a01.\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides,\u00a0head\u00a0of\u00a0House\u00a0Atreides\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u00a0headings.\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0O\u2588This\u00a0is\u00a0H5\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0oblit\u2588Header\u00a0level\u00a05\u00a0content.\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H6\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588This\u00a0is\u00a0H4\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588Header\u00a0level\u00a04\u00a0content.\u00a0Drilling\u00a0down\u00a0in\u00a0to\u00a0finer\u00a0headings.\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H5\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588Header\u00a0level\u00a05\u00a0content.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588This\u00a0is\u00a0H6\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.

    There are 4 screens in the above screenshot, one for the base screen and one for each of the three dialogs. Note how each screen modifies the color of the screen below, but leaves everything visible.

    See the docs on screen opacity if you want to add this to your apps.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#option-list","title":"Option list","text":"

    Textual has had a ListView widget for a while, which is an excellent way of navigating a list of items (actually other widgets). In this release we've added an OptionList which is similar in appearance, but uses the line api under the hood. The Line API makes it more efficient when you approach thousands of items.

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    The Options List accepts Rich renderable, which means that anything Rich can render may be displayed in a list. Here's an Option List of tables:

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hephaestus\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25026.7\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hades\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Caprica\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    We plan to build on the OptionList widget to implement drop-downs, menus, check lists, etc. But it is still very useful as it is, and you can add it to apps now.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#what-else","title":"What else?","text":"

    There are a number of fixes regarding refreshing in this release. If you had issues with parts of the screen not updating, the new version should resolve it.

    There's also a new logging handler, and a \"thick\" border type.

    See release notes for the full details.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#next-week","title":"Next week","text":"

    Next week we plan to take a break from building Textual to building apps with Textual. We do this now and again to give us an opportunity to step back and understand things from the perspective of a developer using Textual. We will hopefully have something interesting to show from the exercise, and new Open Source apps to share.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/","title":"Textual 0.18.0 adds API for managing concurrent workers","text":"

    Less than a week since the last release, and we have a new API to show you.

    This release adds a new Worker API designed to manage concurrency, both asyncio tasks and threads.

    An API to manage concurrency may seem like a strange addition to a library for building user interfaces, but on reflection it makes a lot of sense. People are building Textual apps to interface with REST APIs, websockets, and processes; and they are running into predictable issues. These aren't specifically Textual problems, but rather general problems related to async tasks and threads. It's not enough for us to point users at the asyncio docs, we needed a better answer.

    The new run_worker method provides an easy way of launching \"Workers\" (a wrapper over async tasks and threads) which also manages their lifetime.

    One of the challenges I've found with tasks and threads is ensuring that they are shut down in an orderly manner. Interestingly enough, Textual already implemented an orderly shutdown procedure to close the tasks that power widgets: children are shut down before parents, all the way up to the App (the root node). The new API piggybacks on to that existing mechanism to ensure that worker tasks are also shut down in the same order.

    Tip

    You won't need to worry about this gnarly issue with the new Worker API.

    I'm particularly pleased with the new @work decorator which can turn a coroutine OR a regular function into a Textual Worker object, by scheduling it as either an asyncio task or a thread. I suspect this will solve 90% of the concurrency issues we see with Textual apps.

    See the Worker API for the details.

    "},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/","title":"Textual 0.23.0 improves message handling","text":"

    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\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    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.

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#on-decorator","title":"On decorator","text":"

    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.pyon_decorator02.pyOutput on_decorator01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n        \"\"\"Handle all button pressed events.\"\"\"\n        if event.button.id == \"bell\":\n            self.bell()\n        elif event.button.has_class(\"toggle\", \"dark\"):\n            self.dark = not self.dark\n        elif event.button.id == \"quit\":\n            self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    1. The message handler is called when any button is pressed
    on_decorator02.py
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    @on(Button.Pressed, \"#bell\")  # (1)!\n    def play_bell(self):\n        \"\"\"Called when the bell button is pressed.\"\"\"\n        self.bell()\n\n    @on(Button.Pressed, \".toggle.dark\")  # (2)!\n    def toggle_dark(self):\n        \"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\n        self.dark = not self.dark\n\n    @on(Button.Pressed, \"#quit\")  # (3)!\n    def quit(self):\n        \"\"\"Called when the quit button is pressed.\"\"\"\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    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.

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#why-didnt-we-do-this-earlier","title":"Why didn't we do this earlier?","text":"

    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!

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/","title":"Textual 0.24.0 adds a Select control","text":"

    Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases. At least until it is deposed by version 0.25.0.

    The highlight of this release is the new Select widget: a very familiar control from the web and desktop worlds. Here's a screenshot and code:

    Output (expanded)select_widget.pyselect.css

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select((line, line) for line in LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    \n
    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#new-styles","title":"New styles","text":"

    This one required new functionality in Textual itself. The \"pull-down\" overlay with options presented a difficulty with the previous API. The overlay needed to appear over any content below it. This is possible (using layers), but there was no simple way of positioning it directly under the parent widget.

    We solved this with a new \"overlay\" concept, which can considered a special layer for user interactions like this Select, but also pop-up menus, tooltips, etc. Widgets styled to use the overlay appear in their natural place in the \"document\", but on top of everything else.

    A second problem we tackled was ensuring that an overlay widget was never clipped. This was also solved with a new rule called \"constrain\". Applying constrain to a widget will keep the widget within the bounds of the screen. In the case of Select, if you expand the options while at the bottom of the screen, then the overlay will be moved up so that you can see all the options.

    These new rules are currently undocumented as they are still subject to change, but you can see them in the Select source if you are interested.

    In a future release these will be finalized and you can confidently use them in your own projects.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#fixes-for-the-on-decorator","title":"Fixes for the @on decorator","text":"

    The new @on decorator is proving popular. To recap, it is a more declarative and finely grained way of dispatching messages. Here's a snippet from the calculator example which uses @on:

        @on(Button.Pressed, \"#plus,#minus,#divide,#multiply\")\n    def pressed_op(self, event: Button.Pressed) -> None:\n        \"\"\"Pressed one of the arithmetic operations.\"\"\"\n        self.right = Decimal(self.value or \"0\")\n        self._do_math()\n        assert event.button.id is not None\n        self.operator = event.button.id\n

    The decorator arranges for the method to be called when any of the four math operation buttons are pressed.

    In 0.24.0 we've fixed some missing attributes which prevented the decorator from working with some messages. We've also extended the decorator to use keywords arguments, so it will match attributes other than control.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#other-fixes","title":"Other fixes","text":"

    There is a surprising number of fixes in this release for just 5 days. See CHANGELOG.md for details.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/","title":"Textual adds Sparklines, Selection list, Input validation, and tool tips","text":"

    It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.

    We've been a little distracted with our \"dogfood\" projects: Frogmouth and Trogon. Both of which hit 1000 Github stars in 24 hours. We will be maintaining / updating those, but it is business as usual for this Textual release (and it's a big one). We have such sights to show you.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#sparkline-widget","title":"Sparkline widget","text":"

    A Sparkline is essentially a mini-plot. Just detailed enough to keep an eye on time-series data.

    SparklineColorsApp \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582

    Colors are configurable, and all it takes is a call to set_interval to make it animate.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#selection-list","title":"Selection list","text":"

    Next up is the SelectionList widget. Essentially a scrolling list of checkboxes. Lots of use cases for this one.

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#tooltips","title":"Tooltips","text":"

    We've added tooltips to Textual widgets.

    The API couldn't be simpler: simply assign a string to the tooltip property on any widget. This string will be displayed after 300ms when you hover over the widget.

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    As always, you can configure how the tooltips will be displayed with CSS.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#input-updates","title":"Input updates","text":"

    We have some quality of life improvements for the Input widget.

    You can now use a simple declarative API to validating input.

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258afoo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e ['Must\u00a0be\u00a0a\u00a0valid\u00a0number.',\u00a0'Value\u00a0is\u00a0not\u00a0even.',\u00a0\"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\"]

    Also in this release is a suggestion API, which will suggest auto completions as you type. Hit right to accept the suggestion.

    Here's a screenshot:

    FruitsApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258astrawberry\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    You could use this API to offer suggestions from a fixed list, or even pull the data from a network request.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#join-us","title":"Join us","text":"

    Development on Textual is fast. We're very responsive to issues and feature requests.

    If you have any suggestions, jump on our Discord server and you may see your feature in the next release!

    "},{"location":"blog/2023/07/03/textual-0290-refactors-dev-tools/","title":"Textual 0.29.0 refactors dev tools","text":"

    It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to get a new version out.

    Version 0.29.0 has shipped with a number of fixes (see the release notes for details), but I'd like to use this post to explain a change we made to how Textual developer tools are distributed.

    Previously if you installed textual[dev] you would get the Textual dev tools plus the library itself. If you were distributing Textual apps and didn't need the developer tools you could drop the [dev].

    We did this because the less dependencies a package has, the fewer installation issues you can expect to get in the future. And Textual is surprisingly lean if you only need to run apps, and not build them.

    Alas, this wasn't quite as elegant solution as we hoped. The dependencies defined in extras wouldn't install commands, so textual was bundled with the core library. This meant that if you installed the Textual package without the [dev] you would still get the textual command on your path but it wouldn't run.

    We solved this by creating two packages: textual contains the core library (with minimal dependencies) and textual-dev contains the developer tools. If you are building Textual apps, you should install both as follows:

    pip install textual textual-dev\n

    That's the only difference. If you run in to any issues feel free to ask on the Discord server!

    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/","title":"Textual 0.30.0 adds desktop-style notifications","text":"

    We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.

    By sheer coincidence we reached 20,000 stars on GitHub today. Now stars don't mean all that much (at least until we can spend them on coffee), but its nice to know that twenty thousand developers thought Textual was interesting enough to hit the \u2605 button. Thank you!

    In other news: we moved office. We are now a stone's throw away from Edinburgh Castle. The office is around three times as big as the old place, which means we have room for wide standup desks and dual monitors. But more importantly we have room for new employees. Don't send your CVs just yet, but we hope to grow the team before the end of the year.

    Exciting times.

    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#new-release","title":"New Release","text":"

    And now, for the main feature. Version 0.30 adds a new notification system. Similar to desktop notifications, it displays a small window with a title and message (called a toast) for a pre-defined number of seconds.

    Notifications are great for short timely messages to add supplementary information for the user. Here it is in action:

    The API is super simple. To display a notification, call notify() with a message and an optional title.

    def on_mount(self) -> None:\n    self.notify(\"Hello, from Textual!\", title=\"Welcome\")\n
    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#textualize-video-channel","title":"Textualize Video Channel","text":"

    In case you missed it; Textualize now has a YouTube channel. Our very own Rodrigo has recorded a video tutorial series on how to build Textual apps. Check it out!

    We will be adding more videos in the near future, covering anything from beginner to advanced topics.

    Don't worry if you prefer reading to watching videos. We will be adding plenty more content to the Textual docs in the near future. Watch this space.

    As always, if you want to discuss anything with the Textual developers, join us on the Discord server.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/","title":"Textual 0.38.0 adds a syntax aware TextArea","text":"

    This is the second big feature release this month after last week's command palette.

    The TextArea has finally landed. I know a lot of folk have been waiting for this one. Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers. It is highly configurable, and looks great.

    Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea. See Things I learned while building Textual's TextArea for some of the challenges he faced.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#scoped-css","title":"Scoped CSS","text":"

    Another notable feature added in 0.38.0 is scoped CSS. A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget.

    Consider the following widget:

    class MyWidget(Widget):\n    DEFAULT_CSS = \"\"\"\n    MyWidget {\n        height: auto;\n        border: magenta;\n    }\n    Label {\n        border: solid green;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"foo\")\n        yield Label(\"bar\")\n

    The author has intended to style the labels in that widget by adding a green border. This does work for the widget in question, but (prior to 0.38.0) the Label rule would style all Labels (including any outside of the widget) \u2014 which was probably not intended.

    With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled. This is almost always what you want, which is why it is enabled by default. If you do want to style something outside of the widget you can set SCOPED_CSS=False (as a classvar).

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#light-and-dark-pseudo-selectors","title":"Light and Dark pseudo selectors","text":"

    We've also made a slight quality of life improvement to the CSS, by adding :light and :dark pseudo selectors. This allows you to change styles depending on whether you have dark mode enabled or not.

    This was possible before, just a little verbose. Here's how you would do it in 0.37.0:

    App.-dark-mode MyWidget Label {\n    ...\n}\n

    In 0.38.0 it's a little more concise and readable:

    MyWidget:dark Label {\n    ...\n}\n
    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#testing-guide","title":"Testing guide","text":"

    Not strictly part of the release, but we've added a guide on testing Textual apps.

    As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential. We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin pytest-textual-snapshot.

    This gives devs powerful tools to ensure the quality of their apps. Let us know your thoughts on that!

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#release-notes","title":"Release notes","text":"

    See the release page for the full details on this release.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#whats-next","title":"What's next?","text":"

    There's lots of features planned over the next few months. One feature I am particularly excited by is a widget to generate plots by wrapping the awesome Plotext library. Check out some early work on this feature:

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#join-us","title":"Join us","text":"

    Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

    "},{"location":"blog/2022/11/08/version-040/","title":"Version 0.4.0","text":"

    We've released version 0.4.0 of Textual.

    As this is the first post tagged with release let me first explain where the blog fits in with releases. We plan on doing a post for every note-worthy release. Which likely means all but the most trivial updates (typos just aren't that interesting). Blog posts will be supplementary to release notes which you will find on the Textual repository.

    Blog posts will give a little more background for the highlights in a release, and a rationale for changes and new additions. We embrace building in public, which means that we would like you to be as up-to-date with new developments as if you were sitting in our office. It's a small office, and you might not be a fan of the Scottish weather (it's dreich), but you can at least be here virtually.

    Release 0.4.0 follows 0.3.0, released on October 31st. Here are the highlights of the update.

    "},{"location":"blog/2022/11/08/version-040/#updated-mount-method","title":"Updated Mount Method","text":"

    The mount method has seen some work. We've dropped the ability to assign an id via keyword attributes, which wasn't terribly useful. Now, an id must be assigned via the constructor.

    The mount method has also grown before and after parameters which tell Textual where to add a new Widget (the default was to add it to the end). Here are a few examples:

    # Mount at the start\nself.mount(Button(id=\"Buy Coffee\"), before=0)\n\n# Mount after a selector\nself.mount(Static(\"Password is incorrect\"), after=\"Dialog Input.-error\")\n\n# Mount after a specific widget\ntweet = self.query_one(\"Tweet\")\nself.mount(Static(\"Consider switching to Mastodon\"), after=tweet)\n

    Textual needs much of the same kind of operations as the JS API exposed by the browser. But we are determined to make this way more intuitive. The new mount method is a step towards that.

    "},{"location":"blog/2022/11/08/version-040/#faster-updates","title":"Faster Updates","text":"

    Textual now writes to stdout in a thread. The upshot of this is that Textual can work on the next update before the terminal has displayed the previous frame.

    This means smoother updates all round! You may notice this when scrolling and animating, but even if you don't, you will have more CPU cycles to play with in your Textual app.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ca3Oa3Fx1MDAxNsff91NkPG8r3fdLZ86cybW5t401SXPmmVx1MDAwZUFUXCKK4WIunX73Z0FsXHUwMDAwI2pcdTAwMTKDTPLCKlx1MDAxYvZebNaP/1prQ39/WFurhXdDu/Z5rWbfWqbrtHzzpvYx3j6y/cDxXHUwMDA20ESS34FcdTAwMTf5VrJnN1xmh8HnT5/6pt+zw6FrWrYxcoLIdIMwajmeYXn9T05o94P/xZ/HZt/+79Drt0LfSFx1MDAwN6nbLSf0/IexbNfu24MwgN7/XHUwMDBmv9fWfiefXHUwMDE563zbXG7NQce1k1x1MDAwM5Km1ECK1eTWY2+QXHUwMDE4SzDDVFx1MDAxMk7Q41x1MDAxZU6wXHUwMDA144V2XHUwMDBimttgs522xJtq55fW8dVgOzo971xuWd857UaDzct02Lbjuo3wzn2YXG7T6kZ+xqgg9L2efea0wi6044ntj8dcdTAwMDVcdTAwMWXMQnqU70Wd7sBcdTAwMGWC3DHe0LSc8C7ehlLzXHUwMDFmZuHzWrrlXHUwMDE2filuKClcdTAwMTVcdTAwMTVCYc6kXHUwMDE0j63J8ZxcdTAwMWGEU41cdTAwMDThXGL+Ju3a9FxcuFx1MDAxNGDXf6hgbUumll2aVq9cdTAwMDPmXHJa6T6EqEtbKJXudfP3fJkyiJSUUclcdTAwMWZcdTAwMWK7ttPphtDKkKGlXCJgI5KYXHUwMDExRVMr7ORqxC2USMZcdTAwMWVcdTAwMWLioYd7rcQx/pmczK7pXHUwMDBmx5NWXHUwMDBi4lx1MDAxZlx1MDAxObNji7cnvSrrWZlcdTAwMGJ+2T5qXHUwMDFjOMdcciFcdTAwMWPinp9uXHUwMDFmXHUwMDA218HPx75yblx1MDAxONq3Ye2x4c/H6d3m9v646IBpt+Nv6Vx0R8OW+eCwWFxiqWGKJaY09Vx1MDAwMNdcdTAwMTn0oHFcdTAwMTC5brrNs3qpj3/I2PtMuDQthotKXHUwMDA03pa5YPPgut9ccr5aR0PyvVlf92+PLbp/9l1UXHUwMDFkrjp4q0GRXHUwMDE2XHUwMDA0cy01opzk8FwiXGJcdTAwMWKEUnBuJqTChFx1MDAxN+LF27Rlsdl42YJphabhRVxiM4RmkjMmgFx1MDAxZq5ewFx1MDAxONxcdTAwMGaVwHBcdTAwMDaqXFzIml/I/p55MrpAw9b1tT/sbl37J29cdNn0XHUwMDAxn1x1MDAwM1x1MDAxOUwkUrxcdTAwMTTIXHUwMDE4xkWQYSm4RIhxsTBkw1x1MDAxZt++7l1cdTAwMWW1aGRcdTAwMWTgId9wej/vulWHjFx0XHUwMDBljGFcdTAwMDJcdTAwMTOviKSgXCJcdTAwMTNcdTAwMTImXGbMlVx1MDAxNFx1MDAxOCeuzYohW6mGYaQg2lx1MDAxMFx1MDAxNJNy+dq17qXd22t2b2lvyLqX9uHoXHUwMDFifku+plx1MDAwZlhRXHUwMDExY1pcdTAwMTbxJZhcIkhgtLiG3e2qKPq1dVx1MDAxZlx1MDAxYzXNkfn91t05cdpVx4tQaVx1MDAwMFVcdTAwMTju/pJzzSfo0spA0FwiXHUwMDE555RLnLnZVE/CXGLinCnKsChcdTAwMTexg63v6Ng+6nxcdTAwMGbCbvO62d7mqH/2lohNXHUwMDFmcHWI5ezMJoikXGIuTFx1MDAxOIeQnujFxWt2oFBRuupCXHUwMDE4TIBzM1xuai2QyONFkDZcYokjyPiCKKqXXHUwMDExIT6FKyOZjzRlg9ExPlx1MDAwMJVcdTAwMTBYv4VCLTOaSi+5N1xiXHUwMDFizn2ScaDc1lx1MDAxZLPvuHe5q5b4aOxGyfjZeVxmbFx1MDAxODNxSpXbe911OrFcdTAwMTfXLDhcdTAwMGLbzzl46Fim+7hD32m1svpigVx0JvTp7y2S23i+03FcdTAwMDam+yNv4YuIK6x3MKmpQGRxMZud/FZcdTAwMTQ3TJmBsODgUFxcQFx1MDAwMqry+Vx1MDAxOFx1MDAxNpCPMVxu07BENXuKWyZcdTAwMGKcgVx1MDAxYqaEXHUwMDBirbF8XHUwMDAztVrmnf91uJ35Tsm0zSnTTdL2YOCLYCvOzTjHXHUwMDFhJluphXGbXHUwMDFkQ1RcdTAwMTQ3iqnBXHUwMDE1Y5prXHUwMDA0mZGarH5cYlx1MDAwM0muYFx1MDAxZYA1ls18ViFuXHUwMDEwNCrMdbniVjJtK1x1MDAxMLc5SU8p4iYgZ6BU4cWDydlZcUVx44BcdTAwMWKjWsPpclx1MDAxOVdCJsSNXHUwMDE5XHUwMDE4JE/qh1xmSqo3wW1BcVx1MDAwMyvB0pJDyXevbXNcbngv0LbZ1ZHMZE4gJ1x0QpqpZ1x1MDAwNJT7I3SAdjZa/kE/XHUwMDFhnUdHfp1wXXXkMKKGXHUwMDAy5WJcYkI1hjjOIcewNFx1MDAwNChcdTAwMWZCY5ErJG7FpUfQZ1x1MDAwMVx1MDAwMW/JdVx1MDAxMeF87TaO2DpB9+bJSe8wwvhm9Jq6yFx1MDAxYnU7r9wyfcC02/G3alQ0OS1cXJbjRGhCsmvAc2Wy8+XQPJDfnOZF65c2zZsrc++u6szWMUlcdTAwMTa9OTi8oFwiXHUwMDBlTfPUXG6QUak4pH/JqncxtasvaWKCQEhFdpGnXHUwMDE0dCN2u3s9ijbcXHUwMDAzT9zwweHVcc/uvVx1MDAxZd2ldztcdTAwMGbd6Vx1MDAwM1ZcdTAwMTVdrYvQJZpoXHIh7uJye9A7adCGa91ftK9+7Gy2T5v3vdOqo0spMTAnXHUwMDAwLlx1MDAxMVxmXCJZzfLkYmFcYlx1MDAxZcdcdTAwMWRcdTAwMGbAZG51lVx1MDAxMlxcolC85l82tPvWyW5k4u0uP6xcdTAwMWaMblx1MDAwNlx1MDAwM1Cr6PXQLr3bedBOXHUwMDFmsKLQSsSLoMVcdTAwMWHFSal+XHUwMDA2tdH2VXR4tyt/ycPG9lx1MDAxNW9IsnfxperUYsRcZsqFwpJSXHUwMDExL2boPLWgt5CfYyrwPGorILiSY1x1MDAxNa9Olczu1sXXy0NN7ky457Gj4O6871xmL1/P7tK7ncfu9Fx1MDAwMVfHbmHtVlx1MDAxND9YoyA4XHUwMDA0aJ+R286OayqKbZ0jXHUwMDAzUYRcdTAwMTVcdTAwMDI448WSiYdDmcRcdTAwMDZcdTAwMTNSUPqQ3L7Rasli9du4sqWIxO+7orSC+u2cXHUwMDE0b5n1W4xcdTAwMGLjWy21ZOCKiyM3u1xuUFHkMMeGjlx1MDAxN/0gxGVxOWmCOEhcXLlcdTAwMDYh1VgnXG62yvXJeMVcdTAwMDRcdKrouyau/Fx1MDAxYe6cOuhcdTAwMTLXJ8lcZt5cdTAwMTSDm756xuM3syOJqvKmkEFcdTAwMTDDXHUwMDFh4jnIx8hEOlx0XHUwMDAyJyghT+PF8vVNQ85cdTAwMGKRqX7XXHUwMDBmXHUwMDAzrEDe5iRUS5U3XZxcdFIpiFr8VaPZKXdFYaMxbCBtTGGqp1x1MDAxNG/i7IwhJMRY2+Sb4Lbg+iRcdTAwMDfkJc247jvErXxtm1N0XFyitlx1MDAxNT5cdTAwMGKAXHUwMDA1XCKCaJEpy8yj7eJLdK+Q02tcXHX3XHUwMDFhO1dnW0PvdLfqtGFFXGZcdTAwMTRnZVx1MDAwNCdcdTAwMGZcdTAwMDPofCippVx1MDAwMUmdYip+qlx1MDAxM9LZV7FWXFwoXHUwMDE1U2osT2mjXHUwMDA0bn/wUX4kSXRptG3Ybc9/XHUwMDFlbq7dXHUwMDBlZ8BcdTAwMTZ6w1wi0nJnMYnV2JJcdTAwMTdxVfg6XHUwMDA0ZkzHIUqmejePK9usb1x1MDAwNH3xrd+MzlG9j++3tjevK8+VIIZcdTAwMDBqXG64ojxeoVx1MDAxMFxuKVx1MDAxND9BOut1o9eRlcn9ZpAlKKVSKVa+jpVI1no7J0qrXHUwMDAz68GQmVxcmb7v3UxNxlDhujxcdTAwMDQjSHKK9OJoscb53k7n+HJ9py72/P3dU7y19cKHaSafynzDV424QWAvTjWjkIzhiXSMXCJlcFx1MDAwMOLvXHUwMDAzbK9Lx4rJgsFccqD872NymE5J0LChMFx1MDAwNKtiSuSIOFFcdTAwMWGJXHUwMDE3vIueWFg2cUFo+uGGM2g5g87kIfagVdDimkG46fX7TlxiZnzznEE4uUfS73rs7V3bfEJcdTAwMGX0nG2rhb4zsYI2jDvNL4uk39ZSz0l+PH7/5+PUvVx0llx1MDAwNlI4juhxPreI/+rI0EhcdTAwMDGAmPB5PVx1MDAxNTtH0lPqXHUwMDE3aUdcdTAwMWay/z5Xb0XxS/SIKU3gfNLLPrdGc/Szuf+r88VcdTAwMTXO6c9r+/asqe2rqt9cdTAwMTXqXFxcdTAwMTlcdTAwMTBcdTAwMWRcblx1MDAxZf+PXHUwMDFjQiOevytAKG9gzcZcdTAwMDXRV69CzLgtLCS4mFx0grR8529t/HD6lYhkXHUwMDEzO1x1MDAxZbj6MEa2Zlx1MDAwZYeNMC7SfFx1MDAxZVNcdTAwMDZcdTAwMTfAaY1PMe2tNnLsm40pXHUwMDFl0E7+4l5cdTAwMTNWYyrsePp///nw51+TJY25In0= UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime"},{"location":"blog/2022/11/08/version-040/#multiple-css-paths","title":"Multiple CSS Paths","text":"

    Up to version 0.3.0, Textual would only read a single CSS file set in the CSS_PATH class variable. You can now supply a list of paths if you have more than one CSS file.

    This change was prompted by tuilwindcss which brings a TailwindCSS like approach to building Textual Widgets. Also check out calmcode.io by the same author, which is an amazing resource.

    "},{"location":"blog/2022/12/11/version-060/","title":"Textual 0.6.0 adds a treemendous new widget","text":"

    A new release of Textual lands 3 weeks after the previous release -- and it's a big one.

    Information

    If you're new here, Textual is TUI framework for Python.

    "},{"location":"blog/2022/12/11/version-060/#tree-control","title":"Tree Control","text":"

    The headline feature of version 0.6.0 is a new tree control built from the ground-up. The previous Tree control suffered from an overly complex API and wasn't scalable (scrolling slowed down with 1000s of nodes).

    This new version has a simpler API and is highly scalable (no slowdown with larger trees). There are also a number of visual enhancements in this version.

    Here's a very simple example:

    Outputtree.py

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\n\n\nclass TreeApp(App):\n    def compose(self) -> ComposeResult:\n        tree: Tree[dict] = Tree(\"Dune\")\n        tree.root.expand()\n        characters = tree.root.add(\"Characters\", expand=True)\n        characters.add_leaf(\"Paul\")\n        characters.add_leaf(\"Jessica\")\n        characters.add_leaf(\"Chani\")\n        yield tree\n\n\nif __name__ == \"__main__\":\n    app = TreeApp()\n    app.run()\n

    Here's the tree control being used to navigate some JSON (json_tree.py in the examples directory).

    I'm biased of course, but I think this terminal based tree control is more usable (and even prettier) than just about anything I've seen on the web or desktop. So much of computing tends to organize itself in to a tree that I think this widget will find a lot of uses.

    The Tree control forms the foundation of the DirectoryTree widget, which has also been updated. Here it is used in the code_browser.py example:

    "},{"location":"blog/2022/12/11/version-060/#list-view","title":"List View","text":"

    We have a new ListView control to navigate and select items in a list. Items can be widgets themselves, which makes this a great platform for building more sophisticated controls.

    Outputlist_view.pylist_view.css

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    \n
    "},{"location":"blog/2022/12/11/version-060/#placeholder","title":"Placeholder","text":"

    The Placeholder widget was broken since the big CSS update. We've brought it back and given it a bit of a polish.

    Use this widget in place of custom widgets you have yet to build when designing your UI. The colors are automatically cycled to differentiate one placeholder from the next. You can click a placeholder to cycle between its ID, size, and lorem ipsum text.

    Outputplaceholder.pyplaceholder.css

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3 #p5Placeholder Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0 Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0 33\u00a0x\u00a011nec\u00a0libero\u00a0quis\u00a0gravida.\u00a034\u00a0x\u00a011 Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0 Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0 sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0 amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus50\u00a0x\u00a011 sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis. Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibusimperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedsit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapienlacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass PlaceholderApp(App):\n    CSS_PATH = \"placeholder.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield VerticalScroll(\n            Container(\n                Placeholder(\"This is a custom label for p1.\", id=\"p1\"),\n                Placeholder(\"Placeholder p2 here!\", id=\"p2\"),\n                Placeholder(id=\"p3\"),\n                Placeholder(id=\"p4\"),\n                Placeholder(id=\"p5\"),\n                Placeholder(),\n                Horizontal(\n                    Placeholder(variant=\"size\", id=\"col1\"),\n                    Placeholder(variant=\"text\", id=\"col2\"),\n                    Placeholder(variant=\"size\", id=\"col3\"),\n                    id=\"c1\",\n                ),\n                id=\"bot\",\n            ),\n            Container(\n                Placeholder(variant=\"text\", id=\"left\"),\n                Placeholder(variant=\"size\", id=\"topright\"),\n                Placeholder(variant=\"text\", id=\"botright\"),\n                id=\"top\",\n            ),\n            id=\"content\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = PlaceholderApp()\n    app.run()\n
    \n
    "},{"location":"blog/2022/12/11/version-060/#fixes","title":"Fixes","text":"

    As always, there are a number of fixes in this release. Mostly related to layout. See CHANGELOG.md for the details.

    "},{"location":"blog/2022/12/11/version-060/#whats-next","title":"What's next?","text":"

    The next release will focus on pain points we discovered while in a dog-fooding phase (see the DevLog for details on what Textual devs have been building).

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/","title":"Textual 0.37.0 adds a command palette","text":"

    Textual version 0.37.0 has landed! The highlight of this release is the new command palette.

    A command palette gives users quick access to features in your app. If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands. The commands are matched with a fuzzy search, so you only need to type two or three characters to get to any command.

    Here's a video of it in action:

    Adding your own commands to the command palette is a piece of cake. Here's the (command) Provider class used in the example above:

    class ColorCommands(Provider):\n    \"\"\"A command provider to select colors.\"\"\"\n\n    async def search(self, query: str) -> Hits:\n        \"\"\"Called for each key.\"\"\"\n        matcher = self.matcher(query)\n        for color in COLOR_NAME_TO_RGB.keys():\n            score = matcher.match(color)\n            if score > 0:\n                yield Hit(\n                    score,\n                    matcher.highlight(color),\n                    partial(self.app.post_message, SwitchColor(color)),\n                )\n

    And here is how you add a provider to your app:

    class ColorApp(App):\n    \"\"\"Experiment with the command palette.\"\"\"\n\n    COMMANDS = App.COMMANDS | {ColorCommands}\n

    We're excited about this feature because it is a step towards bringing a common user interface to Textual apps.

    Quote

    It's a Textual app. I know this.

    \u2014 You, maybe.

    The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all.

    See the Guide for details on how to work with the command palette.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#what-else","title":"What else?","text":"

    Also in 0.37.0 we have a new Collapsible widget, which is a great way of adding content while avoiding a cluttered screen.

    And of course, bug fixes and other updates. See the release page for the full details.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#whats-next","title":"What's next?","text":"

    Coming very soon, is a new TextEditor widget. This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages. We're expecting that to land next week. Watch this space, or join the Discord server if you want to be the first to try it out.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#join-us","title":"Join us","text":"

    Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/","title":"Letting your cook multitask while bringing water to a boil","text":"

    Whenever you are cooking a time-consuming meal, you want to multitask as much as possible. For example, you do not want to stand still while you wait for a pot of water to start boiling. Similarly, you want your applications to remain responsive (i.e., you want the cook to \u201cmultitask\u201d) while they do some time-consuming operations in the background (e.g., while the water heats up).

    The animation below shows an example of an application that remains responsive (colours on the left still change on click) even while doing a bunch of time-consuming operations (shown on the right).

    In this blog post, I will teach you how to multitask like a good cook.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#wasting-time-staring-at-pots","title":"Wasting time staring at pots","text":"

    There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve. Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file. The first time I had to do something like this, I ended up writing an application that \u201cblocked\u201d. This means that while the application was reading and parsing the data, nothing else worked.

    To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data. After the data is ready, we display a Label on the right that says that the data has been loaded. On the left, the app has a big rectangle (a custom widget called ColourChanger) that you can click and that changes background colours randomly.

    When you start the application, you can click the rectangle on the left to change the background colour of the ColourChanger, as the animation below shows:

    However, as soon as you press l to trigger the data loading process, clicking the ColourChanger widget doesn't do anything. The app doesn't respond because it is busy working on the data. This is the code of the app so you can try it yourself:

    import time\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):  # (1)!\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]  # (2)!\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:  # (3)!\n        time.sleep(5)  # (4)!\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\n\n\nMyApp().run()\n
    1. The widget ColourChanger changes colours, randomly, when clicked.
    2. We create a binding to the key l that runs an action that we know will take some time (for example, reading and parsing a huge file).
    3. The method action_load is responsible for starting our time-consuming task and then reporting back.
    4. To simplify things a bit, our \u201ctime-consuming task\u201d is just standing still for 5 seconds.

    I think it is easy to understand why the widget ColourChanger stops working when we hit the time.sleep call if we consider the cooking analogy I have written about before in my blog. In short, Python behaves like a lone cook in a kitchen:

    • the cook can be clever and multitask. For example, while water is heating up and being brought to a boil, the cook can go ahead and chop some vegetables.
    • however, there is only one cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.

    Things like \u201cchopping up vegetables\u201d and \u201cseasoning a salad\u201d are blocking, i.e., they need the cook's time and attention. In the app that I showed above, the call to time.sleep is blocking, so the cook can't go and do anything else until the time interval elapses.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#how-can-a-cook-multitask","title":"How can a cook multitask?","text":"

    It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook. Python is like a very dumb cook who only ever does one thing at a time and waits until each thing is completely done before doing the next thing. So, by default, Python would act like a cook who fills up a pan with water, starts heating the water, and then stands there staring at the water until it starts boiling instead of doing something else. It is by using the module asyncio from the standard library that our cook learns to do other tasks while awaiting the completion of the things they already started doing.

    Textual is an async framework, which means it knows how to interoperate with the module asyncio and this will be the solution to our problem. By using asyncio with the tasks we want to run in the background, we will let the application remain responsive while we load and parse the data we need, or while we crunch the numbers we need to crunch, or while we connect to some slow API over the Internet, or whatever it is you want to do.

    The module asyncio uses the keyword async to know which functions can be run asynchronously. In other words, you use the keyword async to identify functions that contain tasks that would otherwise force the cook to waste time. (Functions with the keyword async are called coroutines.)

    The module asyncio also introduces a function asyncio.create_task that you can use to run coroutines concurrently. So, if we create a coroutine that is in charge of doing the time-consuming operation and then run it with asyncio.create_task, we are well on our way to fix our issues.

    However, the keyword async and asyncio.create_task alone aren't enough. Consider this modification of the previous app, where the method action_load now uses asyncio.create_task to run a coroutine who does the sleeping:

    import asyncio\nimport time\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:  # (1)!\n        asyncio.create_task(self._do_long_operation())  # (2)!\n\n    async def _do_long_operation(self) -> None:  # (3)!\n        time.sleep(5)\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\n\n\nMyApp().run()\n
    1. The action method action_load now defers the heavy lifting to another method we created.
    2. The time-consuming operation can be run concurrently with asyncio.create_task because it is a coroutine.
    3. The method _do_long_operation has the keyword async, so it is a coroutine.

    This modified app also works but it suffers from the same issue as the one before! The keyword async tells Python that there will be things inside that function that can be awaited by the cook. That is, the function will do some time-consuming operation that doesn't require the cook's attention. However, we need to tell Python which time-consuming operation doesn't require the cook's attention, i.e., which time-consuming operation can be awaited, with the keyword await.

    Whenever we want to use the keyword await, we need to do it with objects that are compatible with it. For many things, that means using specialised libraries:

    • instead of time.sleep, one can use await asyncio.sleep;
    • instead of the module requests to make Internet requests, use aiohttp; or
    • instead of using the built-in tools to read files, use aiofiles.
    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#achieving-good-multitasking","title":"Achieving good multitasking","text":"

    To fix the last example application, all we need to do is replace the call to time.sleep with a call to asyncio.sleep and then use the keyword await to signal Python that we can be doing something else while we sleep. The animation below shows that we can still change colours while the application is completing the time-consuming operation.

    CodeAnimation
    import asyncio\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:\n        asyncio.create_task(self._do_long_operation())\n\n    async def _do_long_operation(self) -> None:\n        self.query_one(\"#log\").mount(Label(\"Starting \u23f3\"))  # (1)!\n        await asyncio.sleep(5)  # (2)!\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))  # (3)!\n\n\nMyApp().run()\n
    1. We create a label that tells the user that we are starting our time-consuming operation.
    2. We await the time-consuming operation so that the application remains responsive.
    3. We create a label that tells the user that the time-consuming operation has been concluded.

    Because our time-consuming operation runs concurrently, everything else in the application still works while we await for the time-consuming operation to finish. In particular, we can keep changing colours (like the animation above showed) but we can also keep activating the binding with the key l to start multiple instances of the same time-consuming operation! The animation below shows just this:

    Warning

    The animation GIFs in this blog post show low-quality colours in an attempt to reduce the size of the media files you have to download to be able to read this blog post. If you run Textual locally you will see beautiful colours \u2728

    "},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/","title":"Using Rich Inspect to interrogate Python objects","text":"

    The Rich library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is inspect which is so useful you may want to pip install rich just for this feature.

    The easiest way to describe inspect is that it is Python's builtin help() but easier on the eye (and with a few more features). If you invoke it with any object, inspect will display a nicely formatted report on that object \u2014 which makes it great for interrogating objects from the REPL. Here's an example:

    >>> from rich import inspect\n>>> text_file = open(\"foo.txt\", \"w\")\n>>> inspect(text_file)\n

    Here we're inspecting a file object, but it could be literally anything. You will see the following output in the terminal:

    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    By default, inspect will generate a data-oriented summary with a text representation of the object and its data attributes. You can also add methods=True to show all the methods in the public API. Here's an example:

    >>> inspect(text_file, methods=True)\n
    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502detach\u00a0=def\u00a0detach():Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502fileno\u00a0=def\u00a0fileno():Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502flush\u00a0=def\u00a0flush():Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502isatty\u00a0=def\u00a0isatty():Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502readable\u00a0=def\u00a0readable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):Change\u00a0stream\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502writable\u00a0=def\u00a0writable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    The documentation is summarized by default to avoid generating verbose reports. If you want to see the full unabbreviated help you can add help=True:

    >>> inspect(text_file, methods=True, help=True)\n
    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502encoding\u00a0gives\u00a0the\u00a0name\u00a0of\u00a0the\u00a0encoding\u00a0that\u00a0the\u00a0stream\u00a0will\u00a0be\u2502 \u2502decoded\u00a0or\u00a0encoded\u00a0with.\u00a0It\u00a0defaults\u00a0to\u00a0locale.getencoding().\u2502 \u2502\u2502 \u2502errors\u00a0determines\u00a0the\u00a0strictness\u00a0of\u00a0encoding\u00a0and\u00a0decoding\u00a0(see\u2502 \u2502help(codecs.Codec)\u00a0or\u00a0the\u00a0documentation\u00a0for\u00a0codecs.register)\u00a0and\u2502 \u2502defaults\u00a0to\u00a0\"strict\".\u2502 \u2502\u2502 \u2502newline\u00a0controls\u00a0how\u00a0line\u00a0endings\u00a0are\u00a0handled.\u00a0It\u00a0can\u00a0be\u00a0None,\u00a0'',\u2502 \u2502'\\n',\u00a0'\\r',\u00a0and\u00a0'\\r\\n'.\u00a0\u00a0It\u00a0works\u00a0as\u00a0follows:\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0input,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0universal\u00a0newlines\u00a0mode\u00a0is\u2502 \u2502\u00a0\u00a0enabled.\u00a0Lines\u00a0in\u00a0the\u00a0input\u00a0can\u00a0end\u00a0in\u00a0'\\n',\u00a0'\\r',\u00a0or\u00a0'\\r\\n',\u00a0and\u2502 \u2502\u00a0\u00a0these\u00a0are\u00a0translated\u00a0into\u00a0'\\n'\u00a0before\u00a0being\u00a0returned\u00a0to\u00a0the\u2502 \u2502\u00a0\u00a0caller.\u00a0If\u00a0it\u00a0is\u00a0'',\u00a0universal\u00a0newline\u00a0mode\u00a0is\u00a0enabled,\u00a0but\u00a0line\u2502 \u2502\u00a0\u00a0endings\u00a0are\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u00a0If\u00a0it\u00a0has\u00a0any\u00a0of\u2502 \u2502\u00a0\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0input\u00a0lines\u00a0are\u00a0only\u00a0terminated\u00a0by\u00a0the\u00a0given\u2502 \u2502\u00a0\u00a0string,\u00a0and\u00a0the\u00a0line\u00a0ending\u00a0is\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0output,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u2502 \u2502\u00a0\u00a0translated\u00a0to\u00a0the\u00a0system\u00a0default\u00a0line\u00a0separator,\u00a0os.linesep.\u00a0If\u2502 \u2502\u00a0\u00a0newline\u00a0is\u00a0''\u00a0or\u00a0'\\n',\u00a0no\u00a0translation\u00a0takes\u00a0place.\u00a0If\u00a0newline\u00a0is\u00a0any\u2502 \u2502\u00a0\u00a0of\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u00a0translated\u2502 \u2502\u00a0\u00a0to\u00a0the\u00a0given\u00a0string.\u2502 \u2502\u2502 \u2502If\u00a0line_buffering\u00a0is\u00a0True,\u00a0a\u00a0call\u00a0to\u00a0flush\u00a0is\u00a0implied\u00a0when\u00a0a\u00a0call\u00a0to\u2502 \u2502write\u00a0contains\u00a0a\u00a0newline\u00a0character.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():\u2502 \u2502Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502\u2502 \u2502This\u00a0method\u00a0has\u00a0no\u00a0effect\u00a0if\u00a0the\u00a0file\u00a0is\u00a0already\u00a0closed.\u2502 \u2502detach\u00a0=def\u00a0detach():\u2502 \u2502Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502\u2502 \u2502After\u00a0the\u00a0underlying\u00a0buffer\u00a0has\u00a0been\u00a0detached,\u00a0the\u00a0TextIO\u00a0is\u00a0in\u00a0an\u2502 \u2502unusable\u00a0state.\u2502 \u2502fileno\u00a0=def\u00a0fileno():\u2502 \u2502Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502\u2502 \u2502OSError\u00a0is\u00a0raised\u00a0if\u00a0the\u00a0IO\u00a0object\u00a0does\u00a0not\u00a0use\u00a0a\u00a0file\u00a0descriptor.\u2502 \u2502flush\u00a0=def\u00a0flush():\u2502 \u2502Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502\u2502 \u2502This\u00a0is\u00a0not\u00a0implemented\u00a0for\u00a0read-only\u00a0and\u00a0non-blocking\u00a0streams.\u2502 \u2502isatty\u00a0=def\u00a0isatty():\u2502 \u2502Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502\u2502 \u2502Return\u00a0False\u00a0if\u00a0it\u00a0can't\u00a0be\u00a0determined.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):\u2502 \u2502Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502\u2502 \u2502Read\u00a0from\u00a0underlying\u00a0buffer\u00a0until\u00a0we\u00a0have\u00a0n\u00a0characters\u00a0or\u00a0we\u00a0hit\u00a0EOF.\u2502 \u2502If\u00a0n\u00a0is\u00a0negative\u00a0or\u00a0omitted,\u00a0read\u00a0until\u00a0EOF.\u2502 \u2502readable\u00a0=def\u00a0readable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0read()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):\u2502 \u2502Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502\u2502 \u2502Returns\u00a0an\u00a0empty\u00a0string\u00a0if\u00a0EOF\u00a0is\u00a0hit\u00a0immediately.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):\u2502 \u2502Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502\u2502 \u2502hint\u00a0can\u00a0be\u00a0specified\u00a0to\u00a0control\u00a0the\u00a0number\u00a0of\u00a0lines\u00a0read:\u00a0no\u00a0more\u2502 \u2502lines\u00a0will\u00a0be\u00a0read\u00a0if\u00a0the\u00a0total\u00a0size\u00a0(in\u00a0bytes/characters)\u00a0of\u00a0all\u2502 \u2502lines\u00a0so\u00a0far\u00a0exceeds\u00a0hint.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):\u2502 \u2502Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502\u2502 \u2502This\u00a0also\u00a0does\u00a0an\u00a0implicit\u00a0stream\u00a0flush.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):\u2502 \u2502Change\u00a0stream\u00a0position.\u2502 \u2502\u2502 \u2502Change\u00a0the\u00a0stream\u00a0position\u00a0to\u00a0the\u00a0given\u00a0byte\u00a0offset.\u00a0The\u00a0offset\u00a0is\u2502 \u2502interpreted\u00a0relative\u00a0to\u00a0the\u00a0position\u00a0indicated\u00a0by\u00a0whence.\u00a0\u00a0Values\u2502 \u2502for\u00a0whence\u00a0are:\u2502 \u2502\u2502 \u2502*\u00a00\u00a0--\u00a0start\u00a0of\u00a0stream\u00a0(the\u00a0default);\u00a0offset\u00a0should\u00a0be\u00a0zero\u00a0or\u00a0positive\u2502 \u2502*\u00a01\u00a0--\u00a0current\u00a0stream\u00a0position;\u00a0offset\u00a0may\u00a0be\u00a0negative\u2502 \u2502*\u00a02\u00a0--\u00a0end\u00a0of\u00a0stream;\u00a0offset\u00a0is\u00a0usually\u00a0negative\u2502 \u2502\u2502 \u2502Return\u00a0the\u00a0new\u00a0absolute\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0seek(),\u00a0tell()\u00a0and\u00a0truncate()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502This\u00a0method\u00a0may\u00a0need\u00a0to\u00a0do\u00a0a\u00a0test\u00a0seek().\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):\u2502 \u2502Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502\u2502 \u2502File\u00a0pointer\u00a0is\u00a0left\u00a0unchanged.\u00a0\u00a0Size\u00a0defaults\u00a0to\u00a0the\u00a0current\u00a0IO\u2502 \u2502position\u00a0as\u00a0reported\u00a0by\u00a0tell().\u00a0\u00a0Returns\u00a0the\u00a0new\u00a0size.\u2502 \u2502writable\u00a0=def\u00a0writable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0write()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):\u2502 \u2502Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2502\u2502 \u2502Line\u00a0separators\u00a0are\u00a0not\u00a0added,\u00a0so\u00a0it\u00a0is\u00a0usual\u00a0for\u00a0each\u00a0of\u00a0the\u2502 \u2502lines\u00a0provided\u00a0to\u00a0have\u00a0a\u00a0line\u00a0separator\u00a0at\u00a0the\u00a0end.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    There are a few more arguments to refine the level of detail you need (private methods, dunder attributes etc). You can see the full range of options with this delightful little incantation:

    >>> inspect(inspect)\n

    If you are interested in Rich or Textual, join our Discord server!

    "},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/#addendum","title":"Addendum","text":"

    Here's how to have inspect always available without an explicit import:

    Put this in your pythonrc file: pic.twitter.com/pXTi69ykZL

    \u2014 Tushar Sadhwani (@sadhlife) July 27, 2023"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/","title":"Spinners and progress bars in Textual","text":"

    One of the things I love about mathematics is that you can solve a problem just by guessing the correct answer. That is a perfectly valid strategy for solving a problem. The only thing you need to do after guessing the answer is to prove that your guess is correct.

    I used this strategy, to some success, to display spinners and indeterminate progress bars from Rich in Textual.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-an-indeterminate-progress-bar-in-textual","title":"Display an indeterminate progress bar in Textual","text":"

    I have been playing around with Textual and recently I decided I needed an indeterminate progress bar to show that some data was loading. Textual is likely to get progress bars in the future, but I don't want to wait for the future! I want my progress bars now! Textual builds on top of Rich, so if Rich has progress bars, I reckoned I could use them in my Textual apps.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#progress-bars-in-rich","title":"Progress bars in Rich","text":"

    Creating a progress bar in Rich is as easy as opening up the documentation for Progress and copying & pasting the code.

    CodeOutput
    import time\nfrom rich.progress import track\n\nfor _ in track(range(20), description=\"Processing...\"):\n    time.sleep(0.5)  # Simulate work being done\n

    The function track provides a very convenient interface for creating progress bars that keep track of a well-specified number of steps. In the example above, we were keeping track of some task that was going to take 20 steps to complete. (For example, if we had to process a list with 20 elements.) However, I am looking for indeterminate progress bars.

    Scrolling further down the documentation for rich.progress I found what I was looking for:

    CodeOutput
    import time\nfrom rich.progress import Progress\n\nwith Progress() as progress:\n    _ = progress.add_task(\"Loading...\", total=None)  # (1)!\n    while True:\n        time.sleep(0.01)\n
    1. Setting total=None is what makes it an indeterminate progress bar.

    So, putting an indeterminate progress bar on the screen is easy. Now, I only needed to glue that together with the little I know about Textual to put an indeterminate progress bar in a Textual app.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#guessing-what-is-what-and-what-goes-where","title":"Guessing what is what and what goes where","text":"

    What I want is to have an indeterminate progress bar inside my Textual app. Something that looks like this:

    The GIF above shows just the progress bar. Obviously, the end goal is to have the progress bar be part of a Textual app that does something.

    So, when I set out to do this, my first thought went to the stopwatch app in the Textual tutorial because it has a widget that updates automatically, the TimeDisplay. Below you can find the essential part of the code for the TimeDisplay widget and a small animation of it updating when the stopwatch is started.

    TimeDisplay widgetOutput
    from time import monotonic\n\nfrom textual.reactive import reactive\nfrom textual.widgets import Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n

    The reason the time display updates magically is due to the three methods that I highlighted in the code above:

    1. The method on_mount is called when the TimeDisplay widget is mounted on the app and, in it, we use the method set_interval to let Textual know that every 1 / 60 seconds we would like to call the method update_time. (In other words, we would like update_time to be called 60 times per second.)
    2. In turn, the method update_time (which is called automatically a bunch of times per second) will update the reactive attribute time. When this attribute update happens, the method watch_time kicks in.
    3. The method watch_time is a watcher method and gets called whenever the attribute self.time is assigned to. So, if the method update_time is called a bunch of times per second, the watcher method watch_time is also called a bunch of times per second. In it, we create a nice representation of the time that has elapsed and we use the method update to update the time that is being displayed.

    I thought it would be reasonable if a similar mechanism needed to be in place for my progress bar, but then I realised that the progress bar seems to update itself... Looking at the indeterminate progress bar example from before, the only thing going on was that we used time.sleep to stop our program for a bit. We didn't do anything to update the progress bar... Look:

    with Progress() as progress:\n    _ = progress.add_task(\"Loading...\", total=None)  # (1)!\n    while True:\n        time.sleep(0.01)\n

    After pondering about this for a bit, I realised I would not need a watcher method for anything. The watcher method would only make sense if I needed to update an attribute related to some sort of artificial progress, but that clearly isn't needed to get the bar going...

    At some point, I realised that the object progress is the object of interest. At first, I thought progress.add_task would return the progress bar, but it actually returns the integer ID of the task added, so the object of interest is progress. Because I am doing nothing to update the bar explicitly, the object progress must be updating itself.

    The Textual documentation also says that we can build widgets from Rich renderables, so I concluded that if Progress were a renderable, then I could inherit from Static and use the method update to update the widget with my instance of Progress directly. I gave it a try and I put together this code:

    from rich.progress import Progress, BarColumn\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass IndeterminateProgress(Static):\n    def __init__(self):\n        super().__init__(\"\")\n        self._bar = Progress(BarColumn())  # (1)!\n        self._bar.add_task(\"\", total=None)  # (2)!\n\n    def on_mount(self) -> None:\n        # When the widget is mounted start updating the display regularly.\n        self.update_render = self.set_interval(\n            1 / 60, self.update_progress_bar\n        )  # (3)!\n\n    def update_progress_bar(self) -> None:\n        self.update(self._bar)  # (4)!\n\n\nclass MyApp(App):\n    def compose(self) -> ComposeResult:\n        yield IndeterminateProgress()\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    1. Create an instance of Progress that just cares about the bar itself (Rich progress bars can have a label, an indicator for the time left, etc).
    2. We add the indeterminate task with total=None for the indeterminate progress bar.
    3. When the widget is mounted on the app, we want to start calling update_progress_bar 60 times per second.
    4. To update the widget of the progress bar we just call the method Static.update with the Progress object because self._bar is a Rich renderable.

    And lo and behold, it worked:

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#proving-it-works","title":"Proving it works","text":"

    I finished writing this piece of code and I was ecstatic because it was working! After all, my Textual app starts and renders the progress bar. And so, I shared this simple app with someone who wanted to do a similar thing, but I was left with a bad taste in my mouth because I couldn't really connect all the dots and explain exactly why it worked.

    Plot twist

    By the end of the blog post, I will be much closer to a full explanation!

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-a-rich-spinner-in-a-textual-app","title":"Display a Rich spinner in a Textual app","text":"

    A day after creating my basic IndeterminateProgress widget, I found someone that was trying to display a Rich spinner in a Textual app. Actually, it was someone that had filed an issue against Rich. They didn't ask \u201chow can I display a Rich spinner in a Textual app?\u201d, but they filed an alleged bug that crept up on them when they tried displaying a spinner in a Textual app.

    When reading the issue I realised that displaying a Rich spinner looked very similar to displaying a Rich progress bar, so I made a tiny change to my code and tried to run it:

    CodeSpinner running
    from rich.spinner import Spinner\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass SpinnerWidget(Static):\n    def __init__(self):\n        super().__init__(\"\")\n        self._spinner = Spinner(\"moon\")  # (1)!\n\n    def on_mount(self) -> None:\n        self.update_render = self.set_interval(1 / 60, self.update_spinner)\n\n    def update_spinner(self) -> None:\n        self.update(self._spinner)\n\n\nclass MyApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield SpinnerWidget()\n\n\nMyApp().run()\n
    1. Instead of creating an instance of Progress, we create an instance of Spinner and save it so we can call self.update(self._spinner) later on.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#losing-the-battle-against-pausing-the-animations","title":"Losing the battle against pausing the animations","text":"

    After creating the progress bar and spinner widgets I thought of creating the little display that was shown at the beginning of the blog post:

    When writing the code for this app, I realised both widgets had a lot of shared code and logic and I tried abstracting away their common functionality. That led to the code shown below (more or less) where I implemented the updating functionality in IntervalUpdater and then let the IndeterminateProgressBar and SpinnerWidget instantiate the correct Rich renderable.

    from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\n\nfrom textual.app import RenderableType\nfrom textual.widgets import Button, Static\n\n\nclass IntervalUpdater(Static):\n    _renderable_object: RenderableType  # (1)!\n\n    def update_rendering(self) -> None:  # (2)!\n        self.update(self._renderable_object)\n\n    def on_mount(self) -> None:  # (3)!\n        self.interval_update = self.set_interval(1 / 60, self.update_rendering)\n\n\nclass IndeterminateProgressBar(IntervalUpdater):\n    \"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Progress(BarColumn())  # (4)!\n        self._renderable_object.add_task(\"\", total=None)\n\n\nclass SpinnerWidget(IntervalUpdater):\n    \"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\n    def __init__(self, style: str) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Spinner(style)  # (5)!\n
    1. Instances of IntervalUpdate should set the attribute _renderable_object to the instance of the Rich renderable that we want to animate.
    2. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
    3. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
    4. For an indeterminate progress bar we set the attribute _renderable_object to an instance of Progress.
    5. For a spinner we set the attribute _renderable_object to an instance of Spinner.

    But I wanted something more! I wanted to make my app similar to the stopwatch app from the terminal and thus wanted to add a \u201cPause\u201d and a \u201cResume\u201d button. These buttons should, respectively, stop the progress bar and the spinner animations and resume them.

    Below you can see the code I wrote and a short animation of the app working.

    App codeCSSOutput
    from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\n\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Grid, Horizontal, Vertical\nfrom textual.widgets import Button, Static\n\n\nclass IntervalUpdater(Static):\n    _renderable_object: RenderableType\n\n    def update_rendering(self) -> None:\n        self.update(self._renderable_object)\n\n    def on_mount(self) -> None:\n        self.interval_update = self.set_interval(1 / 60, self.update_rendering)\n\n    def pause(self) -> None:  # (1)!\n        self.interval_update.pause()\n\n    def resume(self) -> None:  # (2)!\n        self.interval_update.resume()\n\n\nclass IndeterminateProgressBar(IntervalUpdater):\n    \"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Progress(BarColumn())\n        self._renderable_object.add_task(\"\", total=None)\n\n\nclass SpinnerWidget(IntervalUpdater):\n    \"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\n    def __init__(self, style: str) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Spinner(style)\n\n\nclass LiveDisplayApp(App[None]):\n    \"\"\"App showcasing some widgets that update regularly.\"\"\"\n    CSS_PATH = \"myapp.css\"\n\n    def compose(self) -> ComposeResult:\n        yield Vertical(\n                Grid(\n                    SpinnerWidget(\"moon\"),\n                    IndeterminateProgressBar(),\n                    SpinnerWidget(\"aesthetic\"),\n                    SpinnerWidget(\"bouncingBar\"),\n                    SpinnerWidget(\"earth\"),\n                    SpinnerWidget(\"dots8Bit\"),\n                ),\n                Horizontal(\n                    Button(\"Pause\", id=\"pause\"),  # (3)!\n                    Button(\"Resume\", id=\"resume\", disabled=True),\n                ),\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (4)!\n        pressed_id = event.button.id\n        assert pressed_id is not None\n        for widget in self.query(IntervalUpdater):\n            getattr(widget, pressed_id)()  # (5)!\n\n        for button in self.query(Button):  # (6)!\n            if button.id == pressed_id:\n                button.disabled = True\n            else:\n                button.disabled = False\n\n\nLiveDisplayApp().run()\n
    1. The method pause looks at the attribute interval_update (returned by the method set_interval) and tells it to stop calling the method update_rendering 60 times per second.
    2. The method resume looks at the attribute interval_update (returned by the method set_interval) and tells it to resume calling the method update_rendering 60 times per second.
    3. We set two distinct IDs for the two buttons so we can easily tell which button was pressed and what the press of that button means.
    4. The event handler on_button_pressed will wait for button presses and will take care of pausing or resuming the animations.
    5. We look for all of the instances of IntervalUpdater in our app and use a little bit of introspection to call the correct method (pause or resume) in our widgets. Notice this was only possible because the buttons were assigned IDs that matched the names of the methods. (I love Python !)
    6. We go through our two buttons to disable the one that was just pressed and to enable the other one.
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    height: 1fr;\n    align-horizontal: center;\n}\n\nButton {\n    margin: 0 3 0 3;\n}\n\nGrid {\n    height: 4fr;\n    align: center middle;\n    grid-size: 3 2;\n    grid-columns: 8;\n    grid-rows: 1;\n    grid-gutter: 1;\n    border: gray double;\n}\n\nIntervalUpdater {\n    content-align: center middle;\n}\n

    If you think this was a lot, take a couple of deep breaths before moving on.

    The only issue with my app is that... it does not work! If you press the button to pause the animations, it looks like the widgets are paused. However, you can see that if I move my mouse over the paused widgets, they update:

    Obviously, that caught me by surprise, in the sense that I expected it work. On the other hand, this isn't surprising. After all, I thought I had guessed how I could solve the problem of displaying these Rich renderables that update live and I thought I knew how to pause and resume their animations, but I hadn't convinced myself I knew exactly why it worked.

    Warning

    This goes to show that sometimes it is not the best idea to commit code that you wrote and that works if you don't know why it works. The code might seem to work and yet have deficiencies that will hurt you further down the road.

    As it turns out, the reason why pausing is not working is that I did not grok why the rendering worked in the first place... So I had to go down that rabbit hole first.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#understanding-the-rich-rendering-magic","title":"Understanding the Rich rendering magic","text":""},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-staticupdate-works","title":"How Static.update works","text":"

    The most basic way of creating a Textual widget is to inherit from Widget and implement the method render that just returns the thing that must be printed on the screen. Then, the widget Static provides some functionality on top of that: the method update.

    The method Static.update(renderable) is used to tell the widget in question that its method render (called when the widget needs to be drawn) should just return renderable. So, if the implementation of the method IntervalUpdater.update_rendering (the method that gets called 60 times per second) is this:

    class IntervalUpdater(Static):\n    # ...\n    def update_rendering(self) -> None:\n        self.update(self._renderable_object)\n

    Then, we are essentially saying \u201chey, the thing in self._renderable_object is what must be returned whenever Textual asks you to render yourself. So, this really proves that both Progress and Spinner from Rich are renderables. But what is more, this shows that my implementation of IntervalUpdater can be simplified greatly! In fact, we can boil it down to just this:

    class IntervalUpdater(Static):\n    _renderable_object: RenderableType\n\n    def __init__(self, renderable_object: RenderableType) -> None:  # (1)!\n        super().__init__(renderable_object)  # (2)!\n\n    def on_mount(self) -> None:\n        self.interval_update = self.set_interval(1 / 60, self.refresh)  # (3)!\n
    1. To create an instance of IntervalUpdater, now we give it the Rich renderable that we want displayed. If this Rich renderable is something that updates over time, then those changes will be reflected in the rendering.
    2. We initialise Static with the renderable object itself, instead of initialising with the empty string \"\" and then updating repeatedly.
    3. We call self.refresh 60 times per second. We don't need the auxiliary method update_rendering because this widget (an instance of Static) already knows what its renderable is.

    Once you understand the code above you will realise that the previous implementation of update_rendering was actually doing superfluous work because the repeated calls to self.update always had the exact same object. Again, we see strong evidence that the Rich progress bars and the spinners have the inherent ability to display a different representation of themselves as time goes by.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-rich-spinners-get-updated","title":"How Rich spinners get updated","text":"

    I kept seeing strong evidence that Rich spinners and Rich progress bars updated their own rendering but I still did not have actual proof. So, I went digging around to see how Spinner was implemented and I found this code (from the file spinner.py at the time of writing):

    class Spinner:\n    # ...\n\n    def __rich_console__(\n        self, console: \"Console\", options: \"ConsoleOptions\"\n    ) -> \"RenderResult\":\n        yield self.render(console.get_time())  # (1)!\n\n    # ...\n    def render(self, time: float) -> \"RenderableType\":  # (2)!\n        # ...\n\n        frame_no = ((time - self.start_time) * self.speed) / (  # (3)!\n            self.interval / 1000.0\n        ) + self.frame_no_offset\n        # ...\n\n    # ...\n
    1. The Rich spinner implements the function __rich_console__ that is supposed to return the result of rendering the spinner. Instead, it defers its work to the method render... However, to call the method render, we need to pass the argument console.get_time(), which the spinner uses to know in which state it is!
    2. The method render takes a time and returns a renderable!
    3. To determine the frame number (the current look of the spinner) we do some calculations with the \u201ccurrent time\u201d, given by the parameter time, and the time when the spinner started!

    The snippet of code shown above, from the implementation of Spinner, explains why moving the mouse over a spinner (or a progress bar) that supposedly was paused makes it move. We no longer get repeated updates (60 times per second) because we told our app that we wanted to pause the result of set_interval, so we no longer get automatic updates. However, moving the mouse over the spinners and the progress bar makes Textual want to re-render them and, when it does, it figures out that time was not frozen (obviously!) and so the spinners and the progress bar have a different frame to show.

    To get a better feeling for this, do the following experiment:

    1. Run the command textual console in a terminal to open the Textual devtools console.
    2. Add a print statement like print(\"Rendering from within spinner\") to the beginning of the method Spinner.render (from Rich).
    3. Add a print statement like print(\"Rendering static\") to the beginning of the method Static.render (from Textual).
    4. Put a blank terminal and the devtools console side by side.
    5. Run the app: notice that you get a lot of both print statements.
    6. Hit the Pause button: the print statements stop.
    7. Move your mouse over a widget or two: you get a couple of print statements, one from the Static.render and another from the Spinner.render.

    The result of steps 6 and 7 are shown below. Notice that, in the beginning of the animation, the screen on the right shows some prints but is quiet because no more prints are coming in. When the mouse enters the screen and starts going over widgets, the screen on the right gets new prints in pairs, first from Static.render (which Textual calls to render the widget) and then from Spinner.render because ultimately we need to know how the Spinner looks.

    Now, at this point, I made another educated guess and deduced that progress bars work in the same way! I still have to prove it, and I guess I will do so in another blog post, coming soon, where our spinner and progress bar widgets can be properly paused!

    I will see you soon

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/","title":"Stealing Open Source code from Textual","text":"

    I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't steal a car would you?

    But you should steal code from Open Source projects. Respect the license (you may need to give attribution) but stealing code is not like stealing a car. If I steal your car, I have deprived you of a car. If you steal my open source code, I haven't lost anything.

    Warning

    I'm not advocating for piracy. Open source code gives you explicit permission to use it.

    From my point of view, I feel like code has greater value when it has been copied / modified in another project.

    There are a number of files and modules in Textual that could either be lifted as is, or wouldn't require much work to extract. I'd like to cover a few here. You might find them useful in your next project.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#loop-first-last","title":"Loop first / last","text":"

    How often do you find yourself looping over an iterable and needing to know if an element is the first and/or last in the sequence? It's a simple thing, but I find myself needing this a lot, so I wrote some helpers in _loop.py.

    I'm sure there is an equivalent implementation on PyPI, but steal this if you need it.

    Here's an example of use:

    for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):\n    yield move_to(x, y)\n    yield from line\n    if not last:\n        yield new_line\n
    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#lru-cache","title":"LRU Cache","text":"

    Python's lru_cache can be the one-liner that makes your code orders of magnitude faster. But it has a few gotchas.

    The main issue is managing the lifetime of these caches. The decorator keeps a single global cache, which will keep a reference to every object in the function call. On an instance method that means you keep references to self for the lifetime of your app.

    For a more flexibility you can use the LRUCache implementation from Textual. This uses essentially the same algorithm as the stdlib decorator, but it is implemented as a container.

    Here's a quick example of its use. It works like a dictionary until you reach a maximum size. After that, new elements will kick out the element that was used least recently.

    >>> from textual._cache import LRUCache\n>>> cache = LRUCache(maxsize=3)\n>>> cache[\"foo\"] = 1\n>>> cache[\"bar\"] = 2\n>>> cache[\"baz\"] = 3\n>>> dict(cache)\n{'foo': 1, 'bar': 2, 'baz': 3}\n>>> cache[\"egg\"] = 4\n>>> dict(cache)\n{'bar': 2, 'baz': 3, 'egg': 4}\n

    In Textual, we use a LRUCache to store the results of rendering content to the terminal. For example, in a datatable it is too costly to render everything up front. So Textual renders only the lines that are currently visible on the \"screen\". The cache ensures that scrolling only needs to render the newly exposed lines, and lines that haven't been displayed in a while are discarded to save memory.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#color","title":"Color","text":"

    Textual has a Color class which could be extracted in to a module of its own.

    The Color class can parse colors encoded in a variety of HTML and CSS formats. Color object support a variety of methods and operators you can use to manipulate colors, in a fairly natural way.

    Here's some examples in the REPL.

    >>> from textual.color import Color\n>>> color = Color.parse(\"lime\")\n>>> color\nColor(0, 255, 0, a=1.0)\n>>> color.darken(0.8)\nColor(0, 45, 0, a=1.0)\n>>> color + Color.parse(\"red\").with_alpha(0.1)\nColor(25, 229, 0, a=1.0)\n>>> color = Color.parse(\"#12a30a\")\n>>> color\nColor(18, 163, 10, a=1.0)\n>>> color.css\n'rgb(18,163,10)'\n>>> color.hex\n'#12A30A'\n>>> color.monochrome\nColor(121, 121, 121, a=1.0)\n>>> color.monochrome.hex\n'#797979'\n>>> color.hsl\nHSL(h=0.3246187363834423, s=0.8843930635838151, l=0.33921568627450976)\n>>>\n

    There are some very good color libraries in PyPI, which you should also consider using. But Textual's Color class is lean and performant, with no C dependencies.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#geometry","title":"Geometry","text":"

    This may be my favorite module in Textual: geometry.py.

    The geometry module contains a number of classes responsible for storing and manipulating 2D geometry. There is an Offset class which is a two dimensional point. A Region class which is a rectangular region defined by a coordinate and dimensions. There is a Spacing class which defines additional space around a region. And there is a Size class which defines the dimensions of an area by its width and height.

    These objects are used by Textual's layout engine and compositor, which makes them the oldest and most thoroughly tested part of the project.

    There's a lot going on in this module, but the docstrings are quite detailed and have unicode art like this to help explain things.

                  cut_x \u2193\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502        \u2502 \u2502   \u2502\n          \u2502    0   \u2502 \u2502 1 \u2502\n          \u2502        \u2502 \u2502   \u2502\n  cut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502    2   \u2502 \u2502 3 \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#you-should-steal-our-code","title":"You should steal our code","text":"

    There is a lot going on in the Textual Repository. Including a CSS parser, renderer, layout and compositing engine. All written in pure Python. Steal it with my blessing.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/","title":"Things I learned building a text editor for the terminal","text":"

    TextArea is the latest widget to be added to Textual's growing collection. It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.

    Adding a TextArea to your Textual app is as simple as adding this to your compose method:

    yield TextArea()\n

    Enabling syntax highlighting for a language is as simple as:

    yield TextArea(language=\"python\")\n

    Working on the TextArea widget for Textual taught me a lot about Python and my general approach to software engineering. It gave me an appreciation for the subtle functionality behind the editors we use on a daily basis \u2014 features we may not even notice, despite some engineer spending hours perfecting it to provide a small boost to our development experience.

    This post is a tour of some of these learnings.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#vertical-cursor-movement-is-more-than-just-cursor_row","title":"Vertical cursor movement is more than just cursor_row++","text":"

    When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. Editors should maintain the visual column offset where possible, meaning they must account for double-width emoji (sigh \ud83d\ude14) and East-Asian characters.

    Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it arrives at line 3. This is because the 6th character of line 3 visually aligns with the 11th character of line 1.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-from-other-sources-may-move-my-cursor","title":"Edits from other sources may move my cursor","text":"

    There are two ways to interact with the TextArea:

    1. You can type into it.
    2. You can make API calls to edit the content in it.

    In the example below, Hello, world!\\n is repeatedly inserted at the start of the document via the API. Notice that this updates the location of my cursor, ensuring that I don't lose my place.

    This subtle feature should aid those implementing collaborative and multi-cursor editing.

    This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result.

    Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way!

    A TetrisArea white-boarding session.

    Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks Dave!) is what's needed to finally crack a tough problem.

    Many thanks to David Brochart for sending me down this rabbit hole!

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#spending-a-few-minutes-running-a-profiler-can-be-really-beneficial","title":"Spending a few minutes running a profiler can be really beneficial","text":"

    While building the TextArea widget I avoided heavy optimisation work that may have affected readability or maintainability.

    However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were affecting the performance of my code.

    I spent around 30 minutes profiling TextArea using pyinstrument, and the result was a ~97% reduction in the time taken to handle a key press. What an amazing return on investment for such a minimal time commitment!

    \"pyinstrument -r html\" produces this beautiful output.

    pyinstrument unveiled two issues that were massively impacting performance.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#1-reparsing-highlighting-queries-on-each-key-press","title":"1. Reparsing highlighting queries on each key press","text":"

    I was constructing a tree-sitter Query object on each key press, incorrectly assuming it was a low-overhead call. This query was completely static, so I moved it into the constructor ensuring the object was created only once. This reduced key processing time by around 94% - a substantial and very much noticeable improvement.

    This seems obvious in hindsight, but the code in question was written earlier in the project and had been relegated in my mind to \"code that works correctly and will receive less attention from here on out\". pyinstrument quickly brought this code back to my attention and highlighted it as a glaring performance bug.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#2-namedtuples-are-slower-than-i-expected","title":"2. NamedTuples are slower than I expected","text":"

    In Python, NamedTuples are slow to create relative to tuples, and this cost was adding up inside an extremely hot loop which was instantiating a large number of them. pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside NamedTuple.__new__.

    Here's a quick benchmark which constructs 10,000 NamedTuples:

    \u276f hyperfine -w 2 'python sandbox/darren/make_namedtuples.py'\nBenchmark 1: python sandbox/darren/make_namedtuples.py\n  Time (mean \u00b1 \u03c3):      15.9 ms \u00b1   0.5 ms    [User: 12.8 ms, System: 2.5 ms]\n  Range (min \u2026 max):    15.2 ms \u2026  18.4 ms    165 runs\n

    Here's the same benchmark using tuple instead:

    \u276f hyperfine -w 2 'python sandbox/darren/make_tuples.py'\nBenchmark 1: python sandbox/darren/make_tuples.py\n  Time (mean \u00b1 \u03c3):       9.3 ms \u00b1   0.5 ms    [User: 6.8 ms, System: 2.0 ms]\n  Range (min \u2026 max):     8.7 ms \u2026  12.3 ms    256 runs\n

    Switching to tuple resulted in another noticeable increase in responsiveness. Key-press handling time dropped by almost 50%! Unfortunately, this change does impact readability. However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#syntax-highlighting-is-very-different-from-what-i-expected","title":"Syntax highlighting is very different from what I expected","text":"

    In order to support syntax highlighting, we make use of the tree-sitter library, which maintains a syntax tree representing the structure of our document.

    To perform highlighting, we follow these steps:

    1. The user edits the document.
    2. We inform tree-sitter of the location of this edit.
    3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree.
    4. We run a query against the tree to retrieve ranges of text we wish to highlight.
    5. These ranges are mapped to styles (defined by the chosen \"theme\").
    6. These styles to the appropriate text ranges when rendering the widget.

    Cycling through a few of the builtin themes.

    Another benefit that I didn't consider before working on this project is that tree-sitter parsers can also be used to highlight syntax errors in a document. This can be useful in some situations - for example, highlighting mismatched HTML closing tags:

    Highlighting mismatched closing HTML tags in red.

    Before building this widget, I was oblivious as to how we might approach syntax highlighting. Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have been feasible.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-are-replacements","title":"Edits are replacements","text":"

    All single-cursor edits can be distilled into a single behaviour: replace_range. This replaces a range of characters with some text. We can use this one method to easily implement deletion, insertion, and replacement of text.

    • Inserting text is replacing a zero-width range with the text to insert.
    • Pressing backspace (delete left) is just replacing the character behind the cursor with an empty string.
    • Selecting text and pressing delete is just replacing the selected text with an empty string.
    • Selecting text and pasting is replacing the selected text with some other text.

    This greatly simplified my initial approach, which involved unique implementations for inserting and deleting.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#the-line-between-text-area-and-vscode-in-the-terminal","title":"The line between \"text area\" and \"VSCode in the terminal\"","text":"

    A project like this has no clear finish line. There are always new features, optimisations, and refactors waiting to be made.

    So where do we draw the line?

    We want to provide a widget which can act as both a basic multiline text area that anyone can drop into their app, yet powerful and extensible enough to act as the foundation for a Textual-powered text editor.

    Yet, the more features we add, the more opinionated the widget becomes, and the less that users will feel like they can build it into their own thing. Finding the sweet spot between feature-rich and flexible is no easy task.

    I don't think the answer is clear, and I don't believe it's possible to please everyone.

    Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using TextArea in the future!

    "},{"location":"blog/2023/10/04/announcing-textual-plotext/","title":"Announcing textual-plotext","text":"

    It's no surprise that a common question on the Textual Discord server is how to go about producing plots in the terminal. A popular solution that has been suggested is Plotext. While Plotext doesn't directly support Textual, it is easy to use with Rich and, because of this, we wanted to make it just as easy to use in your Textual applications.

    With this in mind we've created textual-plotext: a library that provides a widget for using Plotext plots in your app. In doing this we've tried our best to make it as similar as possible to using Plotext in a conventional Python script.

    Take this code from the Plotext README:

    import plotext as plt\ny = plt.sin() # sinusoidal test signal\nplt.scatter(y)\nplt.title(\"Scatter Plot\") # to apply a title\nplt.show() # to finally plot\n

    The Textual equivalent of this (including everything needed to make this a fully-working Textual application) is:

    from textual.app import App, ComposeResult\n\nfrom textual_plotext import PlotextPlot\n\nclass ScatterApp(App[None]):\n\n    def compose(self) -> ComposeResult:\n        yield PlotextPlot()\n\n    def on_mount(self) -> None:\n        plt = self.query_one(PlotextPlot).plt\n        y = plt.sin() # sinusoidal test signal\n        plt.scatter(y)\n        plt.title(\"Scatter Plot\") # to apply a title\n\nif __name__ == \"__main__\":\n    ScatterApp().run()\n

    When run the result will look like this:

    Aside from a couple of the more far-out plot types1 you should find that everything you can do with Plotext in a conventional script can also be done in a Textual application.

    Here's a small selection of screenshots from a demo built into the library, each of the plots taken from the Plotext README:

    A key design goal of this widget is that you can develop your plots so that the resulting code looks very similar to that in the Plotext documentation. The core difference is that, where you'd normally import the plotext module as plt and then call functions via plt, you instead use the plt property made available by the widget.

    You don't even need to call the build or show functions as textual-plotext takes care of this for you. You can see this in action in the scatter code shown earlier.

    Of course, moving any existing plotting code into your Textual app means you will need to think about how you get the data and when and where you build your plot. This might be where the Textual worker API becomes useful.

    We've included a longer-form example application that shows off the glorious Scottish weather we enjoy here at Textual Towers, with an application that uses workers to pull down weather data from a year ago and plot it.

    If you are an existing Plotext user who wants to turn your plots into full terminal applications, we think this will be very familiar and accessible. If you're a Textual user who wants to add plots to your application, we think Plotext is a great library for this.

    If you have any questions about this, or anything else to do with Textual, feel free to come and join us on our Discord server or in our GitHub discussions.

    1. Right now there's no animated gif or video support.\u00a0\u21a9

    "},{"location":"blog/2023/09/06/what-is-textual-web/","title":"What is Textual Web?","text":"

    If you know us, you will know that we are the team behind Rich and Textual \u2014 two popular Python libraries that work magic in the terminal.

    Note

    Not to mention Rich-CLI, Trogon, and Frogmouth

    Today we are adding one project more to that lineup: textual-web.

    Textual Web takes a Textual-powered TUI and turns it in to a web application. Here's a video of that in action:

    With the textual-web command you can publish any Textual app on the web, making it available to anyone you send the URL to. This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications.

    We're excited about the possibilities here. Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. They can be built by a single developer without any experience with a traditional web stack. All you need is proficiency in Python and a little time to read our lovely docs.

    Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. We plan to do this in a way that allows the same (Python) code to drive those features. For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser.

    Also in the pipeline is PWA support, so you can build terminal apps, web apps, and desktop apps with a single codebase.

    Textual Web is currently in a public beta. Join our Discord server if you would like to help us test, or if you have any questions.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/","title":"To TUI or not to TUI","text":"

    Tech moves pretty fast. If you don\u2019t stop and look around once in a while, you could miss it. And yet some technology feels like it has been around forever.

    Terminals are one of those forever-technologies.

    My interest is in Text User Interfaces: interactive apps that run within a terminal. I spend lot of time thinking about where TUIs might fit within the tech ecosystem, and how much more they could be doing for developers. Hardly surprising, since that is what we do at Textualize.

    Recently I had the opportunity to test how new TUI projects would be received. You can consider these to be \"testing the water\", and hopefully representative of TUI apps in general.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#the-projects","title":"The projects","text":"

    In April we took a break from building Textual, to building apps with Textual. We had three ideas to work on, and three devs to do the work. One idea we parked for later. The other two were so promising we devoted more time to them. Both projects took around three developer-weeks to build, which also included work on Textual itself and standard duties for responding to issues / community requests. We released them in May.

    The first project was Frogmouth, a Markdown browser. I think this TUI does better than the equivalent web experience in many ways. The only notable missing feature is images, and that will happen before too long.

    Here's a screenshot:

    Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    Info

    Quick aside about these \"screenshots\", because its a common ask. They aren't true screenshots, but rather SVGs exported by Textual.

    We posted Frogmouth on Hacker News and Reddit on a Sunday morning (US time). A day later, it had 1,000 stars and lots of positive feedback.

    The second project was Trogon, a library this time. Trogon automatically creates a TUI for command line apps. Same deal: we released it on a Sunday morning, and it reached 1K stars even quicker than Frogmouth.

    Trogon sqlite-utilstransform v3.31Transform\u00a0a\u00a0table\u00a0beyond\u00a0the\u00a0capabilities\u00a0of\u00a0ALTER\u00a0TABLE \u258a\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258e disable-wal\u258a\u258a\u258e\u258e drop-table\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e drop-view\u258a\u258e\u2587\u2587 dump\u258aOptions\u258e duplicate\u258a\u258e enable-counts\u258a--type\u00a0multiple\u00a0<text\u00a0choice>\u258e enable-fts\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e enable-wal\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e extract\u258a\u2502\u258a\u258e\u2502\u258e\u2585\u2585 index-foreign-keys\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e indexes\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e insert\u258a\u2502\u258aSelect\u25b2\u258e\u2502\u258e insert-files\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e install\u258a\u2514\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2518\u258e memory\u258a\u258aSelect\u258e\u258e optimize\u258a\u258aINTEGER\u258e\u258e populate-fts\u258a\u258aTEXT\u258e\u258e query\u258a\u258aFLOAT\u258e\u258e rebuild-fts\u258a\u258a\u258aBLOB\u258e\u258e\u258e reset-counts\u258a\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e\u258e rows\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e schema\u258a+\u00a0value\u258e search\u258aDrop\u00a0this\u00a0column\u258e tables\u258a\u258e transform\u258a--rename\u00a0multiple\u00a0<text\u00a0text>\u258e triggers\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e uninstall\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e upsert\u258a\u2502\u258a\u258e\u2502\u258e vacuum views$\u00a0sqlite-utils\u00a0transform \u00a0CTRL+R\u00a0\u00a0Close\u00a0&\u00a0Run\u00a0\u00a0CTRL+T\u00a0Focus\u00a0Command\u00a0Tree\u00a0\u00a0CTRL+O\u00a0\u00a0Command\u00a0Info\u00a0\u00a0CTRL+S\u00a0\u00a0Search\u00a0\u00a0F1\u00a0\u00a0About\u00a0

    Both of these projects are very young, but off to a great start. I'm looking forward to seeing how far we can taken them.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#wrapping-up","title":"Wrapping up","text":"

    With previous generations of software, TUIs have required a high degree of motivation to build. That has changed with the work that we (and others) have been doing. A TUI can be a powerful and maintainable piece of software which works as a standalone project, or as a value-add to an existing project.

    As a forever-technology, a TUI is a safe bet.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#discord","title":"Discord","text":"

    Want to discuss this post with myself or other Textualize devs? Join our Discord server...

    "},{"location":"css_types/","title":"CSS Types","text":"

    CSS types define the values that Textual CSS styles accept.

    CSS types will be linked from within the styles reference in the \"Formal Syntax\" section of each style. The CSS types will be denoted by a keyword enclosed by angle brackets < and >.

    For example, the style align-horizontal references the CSS type <horizontal>:

    \nalign-horizontal: <horizontal>;\n
    "},{"location":"css_types/border/","title":"<border>","text":"

    The <border> CSS type represents a border style.

    "},{"location":"css_types/border/#syntax","title":"Syntax","text":"

    The <border> type can take any of the following values:

    Border type Description ascii A border with plus, hyphen, and vertical bar characters. blank A blank border (reserves space for a border). dashed Dashed line border. double Double lined border. heavy Heavy border. hidden Alias for \"none\". hkey Horizontal key-line border. inner Thick solid border. none Disabled border. outer Solid border with additional space around content. round Rounded corners. solid Solid border. tall Solid border with additional space top and bottom. thick Border style that is consistently thick across edges. vkey Vertical key-line border. wide Solid border with additional space left and right."},{"location":"css_types/border/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

    textual borders\n
    "},{"location":"css_types/border/#examples","title":"Examples","text":""},{"location":"css_types/border/#css","title":"CSS","text":"
    #container {\n    border: heavy red;\n}\n\n#heading {\n    border-bottom: solid blue;\n}\n
    "},{"location":"css_types/border/#python","title":"Python","text":"
    widget.styles.border = (\"heavy\", \"red\")\nwidget.styles.border_bottom = (\"solid\", \"blue\")\n
    "},{"location":"css_types/color/","title":"<color>","text":"

    The <color> CSS type represents a color.

    Warning

    Not to be confused with the color CSS rule to set text color.

    "},{"location":"css_types/color/#syntax","title":"Syntax","text":"

    A <color> should be in one of the formats explained in this section. A bullet point summary of the formats available follows:

    • a recognised named color (e.g., red);
    • a 3 or 6 hexadecimal digit number representing the RGB values of the color (e.g., #F35573);
    • a 4 or 8 hexadecimal digit number representing the RGBA values of the color (e.g., #F35573A0);
    • a color description in the RGB system, with or without opacity (e.g., rgb(23, 78, 200));
    • a color description in the HSL system, with or without opacity (e.g., hsl(290, 70%, 80%));

    Textual's default themes also provide many CSS variables with colors that can be used out of the box.

    "},{"location":"css_types/color/#named-colors","title":"Named colors","text":"

    A named color is a <name> that Textual recognises. Below, you can find a (collapsed) list of all of the named colors that Textual recognises, along with their hexadecimal values, their RGB values, and a visual sample.

    All named colors available. colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"css_types/color/#hex-rgb-value","title":"Hex RGB value","text":"

    The hexadecimal RGB format starts with an octothorpe # and is then followed by 3 or 6 hexadecimal digits: 0123456789ABCDEF. Casing is ignored.

    • If 6 digits are used, the format is #RRGGBB:
    • RR represents the red channel;
    • GG represents the green channel; and
    • BB represents the blue channel.
    • If 3 digits are used, the format is #RGB.

    In a 3 digit color, each channel is represented by a single digit which is duplicated when converting to the 6 digit format. For example, the color #A2F is the same as #AA22FF.

    "},{"location":"css_types/color/#hex-rgba-value","title":"Hex RGBA value","text":"

    This is the same as the hex RGB value, but with an extra channel for the alpha component (that sets opacity).

    • If 8 digits are used, the format is #RRGGBBAA, equivalent to the format #RRGGBB with two extra digits for opacity.
    • If 4 digits are used, the format is #RGBA, equivalent to the format #RGB with an extra digit for opacity.
    "},{"location":"css_types/color/#rgb-description","title":"rgb description","text":"

    The rgb format description is a functional description of a color in the RGB color space. This description follows the format rgb(red, green, blue), where red, green, and blue are decimal integers between 0 and 255. They represent the value of the channel with the same name.

    For example, rgb(0, 255, 32) is equivalent to #00FF20.

    "},{"location":"css_types/color/#rgba-description","title":"rgba description","text":"

    The rgba format description is the same as the rgb with an extra parameter for opacity, which should be a value between 0 and 1.

    For example, rgba(0, 255, 32, 0.5) is the color rgb(0, 255, 32) with 50% opacity.

    "},{"location":"css_types/color/#hsl-description","title":"hsl description","text":"

    The hsl format description is a functional description of a color in the HSL color space. This description follows the format hsl(hue, saturation, lightness), where

    • hue is a float between 0 and 360;
    • saturation is a percentage between 0% and 100%; and
    • lightness is a percentage between 0% and 100%.

    For example, the color #00FF20 would be represented as hsl(128, 100%, 50%) in the HSL color space.

    "},{"location":"css_types/color/#hsla-description","title":"hsla description","text":"

    The hsla format description is the same as the hsl with an extra parameter for opacity, which should be a value between 0 and 1.

    For example, hsla(128, 100%, 50%, 0.5) is the color hsl(128, 100%, 50%) with 50% opacity.

    "},{"location":"css_types/color/#examples","title":"Examples","text":""},{"location":"css_types/color/#css","title":"CSS","text":"
    Header {\n    background: red;           /* Color name */\n}\n\n.accent {\n    color: $accent;            /* Textual variable */\n}\n\n#footer {\n    tint: hsl(300, 20%, 70%);  /* HSL description */\n}\n
    "},{"location":"css_types/color/#python","title":"Python","text":"

    In Python, rules that expect a <color> can also accept an instance of the type Color.

    # Mimicking the CSS syntax\nwidget.styles.background = \"red\"           # Color name\nwidget.styles.color = \"$accent\"            # Textual variable\nwidget.styles.tint = \"hsl(300, 20%, 70%)\"  # HSL description\n\nfrom textual.color import Color\n# Using a Color object directly...\ncolor = Color(16, 200, 45)\n# ... which can also parse the CSS syntax\ncolor = Color.parse(\"#A8F\")\n
    "},{"location":"css_types/horizontal/","title":"<horizontal>","text":"

    The <horizontal> CSS type represents a position along the horizontal axis.

    "},{"location":"css_types/horizontal/#syntax","title":"Syntax","text":"

    The <horizontal> type can take any of the following values:

    Value Description center Aligns in the center of the horizontal axis. left (default) Aligns on the left of the horizontal axis. right Aligns on the right of the horizontal axis."},{"location":"css_types/horizontal/#examples","title":"Examples","text":""},{"location":"css_types/horizontal/#css","title":"CSS","text":"
    .container {\n    align-horizontal: right;\n}\n
    "},{"location":"css_types/horizontal/#python","title":"Python","text":"
    widget.styles.align_horizontal = \"right\"\n
    "},{"location":"css_types/integer/","title":"<integer>","text":"

    The <integer> CSS type represents an integer number.

    "},{"location":"css_types/integer/#syntax","title":"Syntax","text":"

    An <integer> is any valid integer number like -10 or 42.

    Note

    Some CSS rules may expect an <integer> within certain bounds. If that is the case, it will be noted in that rule.

    "},{"location":"css_types/integer/#examples","title":"Examples","text":""},{"location":"css_types/integer/#css","title":"CSS","text":"
    .classname {\n    offset: 10 -20\n}\n
    "},{"location":"css_types/integer/#python","title":"Python","text":"

    In Python, a rule that expects a CSS type <integer> will expect a value of the type int:

    widget.styles.offset = (10, -20)\n
    "},{"location":"css_types/keyline/","title":"<keyline>","text":"

    The <keyline> CSS type represents a line style used in the keyline rule.

    "},{"location":"css_types/keyline/#syntax","title":"Syntax","text":"Value Description none No line (disable keyline). thin A thin line. heavy A heavy (thicker) line. double A double line."},{"location":"css_types/keyline/#examples","title":"Examples","text":""},{"location":"css_types/keyline/#css","title":"CSS","text":"
    Vertical {\n    keyline: thin green;\n}\n
    "},{"location":"css_types/keyline/#python","title":"Python","text":"
    # A tuple of <keyline> and color\nwidget.styles.keyline = (\"thin\", \"green\")\n
    "},{"location":"css_types/name/","title":"<name>","text":"

    The <name> type represents a sequence of characters that identifies something.

    "},{"location":"css_types/name/#syntax","title":"Syntax","text":"

    A <name> is any non-empty sequence of characters:

    • starting with a letter a-z, A-Z, or underscore _; and
    • followed by zero or more letters a-zA-Z, digits 0-9, underscores _, and hiphens -.
    "},{"location":"css_types/name/#examples","title":"Examples","text":""},{"location":"css_types/name/#css","title":"CSS","text":"
    Screen {\n    layers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;\n}\n
    "},{"location":"css_types/name/#python","title":"Python","text":"
    widget.styles.layers = \"onlyLetters Letters-and-hiphens _lead-under letters-1-digit\"\n
    "},{"location":"css_types/number/","title":"<number>","text":"

    The <number> CSS type represents a real number, which can be an integer or a number with a decimal part (akin to a float in Python).

    "},{"location":"css_types/number/#syntax","title":"Syntax","text":"

    A <number> is an <integer>, optionally followed by the decimal point . and a decimal part composed of one or more digits.

    "},{"location":"css_types/number/#examples","title":"Examples","text":""},{"location":"css_types/number/#css","title":"CSS","text":"
    Grid {\n    grid-size: 3 6  /* Integers are numbers */\n}\n\n.translucid {\n    opacity: 0.5    /* Numbers can have a decimal part */\n}\n
    "},{"location":"css_types/number/#python","title":"Python","text":"

    In Python, a rule that expects a CSS type <number> will accept an int or a float:

    widget.styles.grid_size = (3, 6)  # Integers are numbers\nwidget.styles.opacity = 0.5       # Numbers can have a decimal part\n
    "},{"location":"css_types/overflow/","title":"<overflow>","text":"

    The <overflow> CSS type represents overflow modes.

    "},{"location":"css_types/overflow/#syntax","title":"Syntax","text":"

    The <overflow> type can take any of the following values:

    Value Description auto Determine overflow mode automatically. hidden Don't overflow. scroll Allow overflowing."},{"location":"css_types/overflow/#examples","title":"Examples","text":""},{"location":"css_types/overflow/#css","title":"CSS","text":"
    #container {\n    overflow-y: hidden;  /* Don't overflow */\n}\n
    "},{"location":"css_types/overflow/#python","title":"Python","text":"
    widget.styles.overflow_y = \"hidden\"  # Don't overflow\n
    "},{"location":"css_types/percentage/","title":"<percentage>","text":"

    The <percentage> CSS type represents a percentage value. It is often used to represent values that are relative to the parent's values.

    Warning

    Not to be confused with the <scalar> type.

    "},{"location":"css_types/percentage/#syntax","title":"Syntax","text":"

    A <percentage> is a <number> followed by the percent sign % (without spaces). Some rules may clamp the values between 0% and 100%.

    "},{"location":"css_types/percentage/#examples","title":"Examples","text":""},{"location":"css_types/percentage/#css","title":"CSS","text":"
    #footer {\n    /* Integer followed by % */\n    color: red 70%;\n\n    /* The number can be negative/decimal, although that may not make sense */\n    offset: -30% 12.5%;\n}\n
    "},{"location":"css_types/percentage/#python","title":"Python","text":"
    # Integer followed by %\nwidget.styles.color = \"red 70%\"\n\n# The number can be negative/decimal, although that may not make sense\nwidget.styles.offset = (\"-30%\", \"12.5%\")\n
    "},{"location":"css_types/scalar/","title":"<scalar>","text":"

    The <scalar> CSS type represents a length. It can be a <number> and a unit, or the special value auto. It is used to represent lengths, for example in the width and height rules.

    Warning

    Not to be confused with the <number> or <percentage> types.

    "},{"location":"css_types/scalar/#syntax","title":"Syntax","text":"

    A <scalar> can be any of the following:

    • a fixed number of cells (e.g., 10);
    • a fractional proportion relative to the sizes of the other widgets (e.g., 1fr);
    • a percentage relative to the container widget (e.g., 50%);
    • a percentage relative to the container width/height (e.g., 25w/75h);
    • a percentage relative to the viewport width/height (e.g., 25vw/75vh); or
    • the special value auto to compute the optimal size to fit without scrolling.

    A complete reference table and detailed explanations follow. You can skip to the examples.

    Unit symbol Unit Example Description \"\" Cell 10 Number of cells (rows or columns). \"fr\" Fraction 1fr Specifies the proportion of space the widget should occupy. \"%\" Percent 75% Length relative to the container widget. \"w\" Width 25w Percentage relative to the width of the container widget. \"h\" Height 75h Percentage relative to the height of the container widget. \"vw\" Viewport width 25vw Percentage relative to the viewport width. \"vh\" Viewport height 75vh Percentage relative to the viewport height. - Auto auto Tries to compute the optimal size to fit without scrolling."},{"location":"css_types/scalar/#cell","title":"Cell","text":"

    The number of cells is the only unit for a scalar that is absolute. This can be an integer or a float but floats are truncated to integers.

    If used to specify a horizontal length, it corresponds to the number of columns. For example, in width: 15, this sets the width of a widget to be equal to 15 cells, which translates to 15 columns.

    If used to specify a vertical length, it corresponds to the number of lines. For example, in height: 10, this sets the height of a widget to be equal to 10 cells, which translates to 10 lines.

    "},{"location":"css_types/scalar/#fraction","title":"Fraction","text":"

    The unit fraction is used to represent proportional sizes.

    For example, if two widgets are side by side and one has width: 1fr and the other has width: 3fr, the second one will be three times as wide as the first one.

    "},{"location":"css_types/scalar/#percent","title":"Percent","text":"

    The percent unit matches a <percentage> and is used to specify a total length relative to the space made available by the container widget.

    If used to specify a horizontal length, it will be relative to the width of the container. For example, width: 50% sets the width of a widget to 50% of the width of its container.

    If used to specify a vertical length, it will be relative to the height of the container. For example, height: 50% sets the height of a widget to 50% of the height of its container.

    "},{"location":"css_types/scalar/#width","title":"Width","text":"

    The width unit is similar to the percent unit, except it sets the percentage to be relative to the width of the container.

    For example, width: 25w sets the width of a widget to 25% of the width of its container and height: 25w sets the height of a widget to 25% of the width of its container. So, if the container has a width of 100 cells, the width and the height of the child widget will be of 25 cells.

    "},{"location":"css_types/scalar/#height","title":"Height","text":"

    The height unit is similar to the percent unit, except it sets the percentage to be relative to the height of the container.

    For example, height: 75h sets the height of a widget to 75% of the height of its container and width: 75h sets the width of a widget to 75% of the height of its container. So, if the container has a height of 100 cells, the width and the height of the child widget will be of 75 cells.

    "},{"location":"css_types/scalar/#viewport-width","title":"Viewport width","text":"

    This is the same as the width unit, except that it is relative to the width of the viewport instead of the width of the immediate container. The width of the viewport is the width of the terminal minus the widths of widgets that are docked left or right.

    For example, width: 25vw will try to set the width of a widget to be 25% of the viewport width, regardless of the widths of its containers.

    "},{"location":"css_types/scalar/#viewport-height","title":"Viewport height","text":"

    This is the same as the height unit, except that it is relative to the height of the viewport instead of the height of the immediate container. The height of the viewport is the height of the terminal minus the heights of widgets that are docked top or bottom.

    For example, height: 75vh will try to set the height of a widget to be 75% of the viewport height, regardless of the height of its containers.

    "},{"location":"css_types/scalar/#auto","title":"Auto","text":"

    This special value will try to calculate the optimal size to fit the contents of the widget without scrolling.

    For example, if its container is big enough, a label with width: auto will be just as wide as its text.

    "},{"location":"css_types/scalar/#examples","title":"Examples","text":""},{"location":"css_types/scalar/#css","title":"CSS","text":"
    Horizontal {\n    width: 60;     /* 60 cells */\n    height: 1fr;   /* proportional size of 1 */\n}\n
    "},{"location":"css_types/scalar/#python","title":"Python","text":"
    widget.styles.width = 16       # Cell unit can be specified with an int/float\nwidget.styles.height = \"1fr\"   # proportional size of 1\n
    "},{"location":"css_types/text_align/","title":"<text-align>","text":"

    The <text-align> CSS type represents alignments that can be applied to text.

    Warning

    Not to be confused with the text-align CSS rule that sets the alignment of text in a widget.

    "},{"location":"css_types/text_align/#syntax","title":"Syntax","text":"

    A <text-align> can be any of the following values:

    Value Alignment type center Center alignment. end Alias for right. justify Text is justified inside the widget. left Left alignment. right Right alignment. start Alias for left.

    Tip

    The meanings of start and end will likely change when RTL languages become supported by Textual.

    "},{"location":"css_types/text_align/#examples","title":"Examples","text":""},{"location":"css_types/text_align/#css","title":"CSS","text":"
    Label {\n    text-align: justify;\n}\n
    "},{"location":"css_types/text_align/#python","title":"Python","text":"
    widget.styles.text_align = \"justify\"\n
    "},{"location":"css_types/text_style/","title":"<text-style>","text":"

    The <text-style> CSS type represents styles that can be applied to text.

    Warning

    Not to be confused with the text-style CSS rule that sets the style of text in a widget.

    "},{"location":"css_types/text_style/#syntax","title":"Syntax","text":"

    A <text-style> can be the value none for plain text with no styling, or any space-separated combination of the following values:

    Value Description bold Bold text. italic Italic text. reverse Reverse video text (foreground and background colors reversed). strike Strikethrough text. underline Underline text."},{"location":"css_types/text_style/#examples","title":"Examples","text":""},{"location":"css_types/text_style/#css","title":"CSS","text":"
    #label1 {\n    /* You can specify any value by itself. */\n    rule: strike;\n}\n\n#label2 {\n    /* You can also combine multiple values. */\n    rule: strike bold italic reverse;\n}\n
    "},{"location":"css_types/text_style/#python","title":"Python","text":"
    # You can specify any value by itself\nwidget.styles.text_style = \"strike\"\n\n# You can also combine multiple values\nwidget.styles.text_style = \"strike bold italic reverse\n
    "},{"location":"css_types/vertical/","title":"<vertical>","text":"

    The <vertical> CSS type represents a position along the vertical axis.

    "},{"location":"css_types/vertical/#syntax","title":"Syntax","text":"

    The <vertical> type can take any of the following values:

    Value Description bottom Aligns at the bottom of the vertical axis. middle Aligns in the middle of the vertical axis. top (default) Aligns at the top of the vertical axis."},{"location":"css_types/vertical/#examples","title":"Examples","text":""},{"location":"css_types/vertical/#css","title":"CSS","text":"
    .container {\n    align-vertical: top;\n}\n
    "},{"location":"css_types/vertical/#python","title":"Python","text":"
    widget.styles.align_vertical = \"top\"\n
    "},{"location":"events/","title":"Events","text":"

    A reference to Textual events.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"events/blur/","title":"Blur","text":"

    The Blur event is sent to a widget when it loses focus.

    • Bubbles
    • Verbose
    "},{"location":"events/blur/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/blur/#code","title":"Code","text":""},{"location":"events/blur/#textual.events.Blur","title":"textual.events.Blur class","text":"

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"events/blur/#see-also","title":"See also","text":"
    • DescendantBlur
    • DescendantFocus
    • Focus
    "},{"location":"events/click/","title":"Click","text":"

    The Click event is sent to a widget when the user clicks a mouse button.

    • Bubbles
    • Verbose
    "},{"location":"events/click/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/click/#code","title":"Code","text":""},{"location":"events/click/#textual.events.Click","title":"textual.events.Click class","text":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/","title":"DescendantBlur","text":"

    The DescendantBlur event is sent to a widget when one of its children loses focus.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/descendant_blur/#code","title":"Code","text":""},{"location":"events/descendant_blur/#textual.events.DescendantBlur","title":"textual.events.DescendantBlur class","text":"

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.control","title":"control property","text":"
    control: Widget\n

    The widget that was blurred (alias of widget).

    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was blurred.

    "},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
    • Blur
    • DescendantFocus
    • Focus
    "},{"location":"events/descendant_focus/","title":"DescendantFocus","text":"

    The DescendantFocus event is sent to a widget when one of its descendants receives focus.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_focus/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/descendant_focus/#code","title":"Code","text":""},{"location":"events/descendant_focus/#textual.events.DescendantFocus","title":"textual.events.DescendantFocus class","text":"

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.control","title":"control property","text":"
    control: Widget\n

    The widget that was focused (alias of widget).

    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was focused.

    "},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
    • Blur
    • DescendantBlur
    • Focus
    "},{"location":"events/enter/","title":"Enter","text":"

    The Enter event is sent to a widget when the mouse pointer first moves over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/enter/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/enter/#code","title":"Code","text":""},{"location":"events/enter/#textual.events.Enter","title":"textual.events.Enter class","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/","title":"Focus","text":"

    The Focus event is sent to a widget when it receives input focus.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/focus/#code","title":"Code","text":""},{"location":"events/focus/#textual.events.Focus","title":"textual.events.Focus class","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#see-also","title":"See also","text":"
    • Blur
    • DescendantBlur
    • DescendantFocus
    "},{"location":"events/hide/","title":"Hide","text":"

    The Hide event is sent to a widget when it is hidden from view.

    • Bubbles
    • Verbose
    "},{"location":"events/hide/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/hide/#code","title":"Code","text":""},{"location":"events/hide/#textual.events.Hide","title":"textual.events.Hide class","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

    "},{"location":"events/key/","title":"Key","text":"

    The Key event is sent to a widget when the user presses a key on the keyboard.

    • Bubbles
    • Verbose
    "},{"location":"events/key/#attributes","title":"Attributes","text":"attribute type purpose key str Name of the key that was pressed. char str or None The character that was pressed, or None it isn't printable."},{"location":"events/key/#code","title":"Code","text":""},{"location":"events/key/#textual.events.Key","title":"textual.events.Key class","text":"
    def __init__(self, key, character):\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description key str required

    The key that was pressed.

    character str | None required

    A printable character or None if it is not printable.

    Attributes Name Type Description aliases list[str]

    The aliases for the key, including the key itself.

    "},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable: bool\n

    Check if the key is printable (produces a unicode character).

    Returns Type Description bool

    True if the key is printable.

    "},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
    name: str\n

    Name of a key suitable for use as a Python identifier.

    "},{"location":"events/key/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
    name_aliases: list[str]\n

    The corresponding name for every alias in aliases list.

    "},{"location":"events/leave/","title":"Leave","text":"

    The Leave event is sent to a widget when the mouse pointer moves off a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/leave/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/leave/#code","title":"Code","text":""},{"location":"events/leave/#textual.events.Leave","title":"textual.events.Leave class","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/load/","title":"Load","text":"

    The Load event is sent to the app prior to switching the terminal to application mode.

    The load event is typically used to do any setup actions required by the app that don't change the display.

    • Bubbles
    • Verbose
    "},{"location":"events/load/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/load/#code","title":"Code","text":""},{"location":"events/load/#textual.events.Load","title":"textual.events.Load class","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/","title":"Mount","text":"

    The Mount event is sent to a widget and Application when it is first mounted.

    The mount event is typically used to set the initial state of a widget or to add new children widgets.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/mount/#code","title":"Code","text":""},{"location":"events/mount/#textual.events.Mount","title":"textual.events.Mount class","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

    The MouseCapture event is sent to a widget when it is capturing mouse events from outside of its borders on the screen.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_capture/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was captured"},{"location":"events/mouse_capture/#code","title":"Code","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture","title":"textual.events.MouseCapture class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Sent when the mouse has been captured.

    • Bubbles
    • Verbose

    When a mouse has been captured, all further mouse events will be sent to the capturing widget.

    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when captured.

    "},{"location":"events/mouse_down/","title":"MouseDown","text":"

    The MouseDown event is sent to a widget when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_down/#code","title":"Code","text":""},{"location":"events/mouse_down/#textual.events.MouseDown","title":"textual.events.MouseDown class","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_move/","title":"MouseMove","text":"

    The MouseMove event is sent to a widget when the mouse pointer is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_move/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_move/#code","title":"Code","text":""},{"location":"events/mouse_move/#textual.events.MouseMove","title":"textual.events.MouseMove class","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_release/","title":"MouseRelease","text":"

    The MouseRelease event is sent to a widget when it is no longer receiving mouse events outside of its borders.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_release/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was released"},{"location":"events/mouse_release/#code","title":"Code","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease","title":"textual.events.MouseRelease class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when released.

    "},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

    The MouseScrollDown event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved down.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_down/#code","title":"Code","text":""},{"location":"events/mouse_scroll_down/#textual.events.MouseScrollDown","title":"textual.events.MouseScrollDown class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

    The MouseScrollUp event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved up.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_up/#code","title":"Code","text":""},{"location":"events/mouse_scroll_up/#textual.events.MouseScrollUp","title":"textual.events.MouseScrollUp class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_up/","title":"MouseUp","text":"

    The MouseUp event is sent to a widget when the user releases a mouse button.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_up/#code","title":"Code","text":""},{"location":"events/mouse_up/#textual.events.MouseUp","title":"textual.events.MouseUp class","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"events/paste/","title":"Paste","text":"

    The Paste event is sent to a widget when the user pastes text.

    • Bubbles
    • Verbose
    "},{"location":"events/paste/#attributes","title":"Attributes","text":"attribute type purpose text str The text that was pasted"},{"location":"events/paste/#code","title":"Code","text":""},{"location":"events/paste/#textual.events.Paste","title":"textual.events.Paste class","text":"
    def __init__(self, text):\n

    Bases: Event

    Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    The text that has been pasted.

    "},{"location":"events/resize/","title":"Resize","text":"

    The Resize event is sent to a widget when its size changes and when it is first made visible.

    • Bubbles
    • Verbose
    "},{"location":"events/resize/#attributes","title":"Attributes","text":"attribute type purpose size Size The new size of the Widget virtual_size Size The virtual size (scrollable area) of the Widget container_size Size The size of the container (parent widget)"},{"location":"events/resize/#code","title":"Code","text":""},{"location":"events/resize/#textual.events.Resize","title":"textual.events.Resize class","text":"
    def __init__(self, size, virtual_size, container_size=None):\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description size Size required

    The new size of the Widget.

    virtual_size Size required

    The virtual size (scrollable size) of the Widget.

    container_size Size | None None

    The size of the Widget's container widget.

    "},{"location":"events/screen_resume/","title":"ScreenResume","text":"

    The ScreenResume event is sent to a Screen when it becomes current.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_resume/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/screen_resume/#code","title":"Code","text":""},{"location":"events/screen_resume/#textual.events.ScreenResume","title":"textual.events.ScreenResume class","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

    The ScreenSuspend event is sent to a Screen when it is replaced by another screen.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/screen_suspend/#code","title":"Code","text":""},{"location":"events/screen_suspend/#textual.events.ScreenSuspend","title":"textual.events.ScreenSuspend class","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"events/show/","title":"Show","text":"

    The Show event is sent to a widget when it becomes visible.

    • Bubbles
    • Verbose
    "},{"location":"events/show/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/show/#code","title":"Code","text":""},{"location":"events/show/#textual.events.Show","title":"textual.events.Show class","text":"

    Bases: Event

    Sent when a widget has become visible.

    • Bubbles
    • Verbose
    "},{"location":"examples/styles/","title":"Index","text":"

    These are the examples from the documentation, used to generate screenshots.

    You can run them with the textual CLI.

    For example:

    textual run text_style.py\n
    "},{"location":"guide/","title":"Guide","text":"

    Welcome to the Textual Guide! An in-depth reference on how to build apps with Textual.

    "},{"location":"guide/#example-code","title":"Example code","text":"

    Most of the code in this guide is fully working\u2014you could cut and paste it if you wanted to.

    Although it is probably easier to check out the Textual repository and navigate to the docs/examples/guide directory and run the examples from there.

    "},{"location":"guide/CSS/","title":"Textual CSS","text":"

    Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed.

    VSCode User?

    The official Textual CSS extension adds syntax highlighting for both external files and inline CSS.

    "},{"location":"guide/CSS/#stylesheets","title":"Stylesheets","text":"

    CSS stands for Cascading Stylesheet. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies styles to widgets, but otherwise it is the same idea.

    Let's look at some Textual CSS.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    This is an example of a CSS rule set. There may be many such sections in any given CSS file.

    Let's break this CSS code down a bit.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    The first line is a selector which tells Textual which widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class Header.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    The lines inside the curly braces contains CSS rules, which consist of a rule name and rule value separated by a colon and ending in a semicolon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semicolons.

    The first rule in the above example reads \"dock: top;\". The rule name is dock which tells Textual to place the widget on an edge of the screen. The text after the colon is top which tells Textual to dock to the top of the screen. Other valid values for dock are \"right\", \"bottom\", or \"left\"; but \"top\" is most appropriate for a header.

    "},{"location":"guide/CSS/#the-dom","title":"The DOM","text":"

    The DOM, or Document Object Model, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure.

    Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These child widgets form the branches of the tree.

    Let's look at a trivial Textual app.

    dom1.pyOutput
    from textual.app import App\n\n\nclass ExampleApp(App):\n    pass\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    ExampleApp

    This example creates an instance of ExampleApp, which will implicitly create a Screen object. In DOM terms, the Screen is a child of ExampleApp.

    With the above example, the DOM will look like the following:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()

    This doesn't look much like a tree yet. Let's add a header and a footer to this application, which will create more branches of the tree:

    dom2.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass ExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    ExampleApp \u2b58ExampleApp

    With a header and a footer widget the DOM looks like this:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1aa0/bSFx1MDAxNP3Or0DZL7tS4877UWm1glx1MDAxNpZ3aWFcdTAwMDNlVVWuPSReP2s7QKj633dsgu28XHUwMDFjXHUwMDEzXHUwMDEyNpXWQiGZmdy5nrnnzLk3/r6xudlKXHUwMDA3kWq92WypO8v0XHUwMDFjOzZvW6+y9lx1MDAxYlx1MDAxNSdOXHUwMDE46C6Uf07CfmzlI3tpXHUwMDFhJW9ev/bN2FVp5JmWMm6cpG96Sdq3ndCwQv+1kyo/+SN7PTF99XtcdTAwMTT6dlx1MDAxYVx1MDAxYuUkbWU7aVx1MDAxOD/MpTzlqyBNtPW/9efNze/5a8U72zH9MLDz4XlHxT0hx1tPwiB3XHUwMDE1Ulx1MDAwNLEkklx1MDAxNFx1MDAwM5zknZ4sVbbuvdZcdTAwMGWrsidralxyTi//7Jn/qItLwY5Odq7hh17nvJz12vG8s3TgPayDafX6sSp7kzRcdTAwMGVddeHYaS+bfKy9+F5cdTAwMTLqJSi/XHUwMDE1h/1uL1BJMvKdMDItJ1x1MDAxZOg2XG6KRjPo5ibKlrtsXHUwMDAxIDVcdTAwMTBcdTAwMTdYMIwpXHUwMDA2RJS3m31fSINRhFx05nTMo7ehp3dAe/RcdTAwMGLIr9Knr6bldrVjgV2OXHUwMDExyJKwcre3j/dZma+nnG4vzVx1MDAxYVx1MDAxMTJcdTAwMDQgTHD6YLuyXHUwMDFhKl99yCDjknAhip5sxmjfzsPg8/jq9cw4XHUwMDFhrlIryT5UvM1cdTAwMWPdXHUwMDE5j6FqXHUwMDFjVXbY/bgtrlx1MDAwZTBoR9euc9x7f+6eqm+FrZGgS9Vd2io6fryqM1x1MDAwYjto76vf+3JLOsHX+Pro+vh+f3u6WTOOw9umdpfu7sjoV00nLM1cdTAwMGXflfvTj2wzXHUwMDFkbilcdTAwMDNcdTAwMDTpXHUwMDAwpITgot9zXHUwMDAyV3dcdTAwMDZ9zyvbQsstMbhR8XdcdTAwMDL5I35WYVx1MDAwZsFM2DMuJEKYoca4r1/mNcU9XHUwMDAydbiHXHUwMDA0XHUwMDFhlOZcdTAwMTB8XHUwMDBl7tPYXGaSyIw1tqZgn0/Dvlx1MDAxY8c6oYBwKjFaPtSXXHUwMDE5h+V2h0F65tw/xJJBNcNcdTAwMDHEXHUwMDAw4lJcdTAwMDLKR0btmr7jXHJGdjBcdTAwMGZY7fnOnelHntqKol9/q65worQnuWky8p0tz+lmgd2y9L2peCTmU0efm8VcdTAwMDDfsW2vXHUwMDEyf5Z2xNQ24/0mZ1hcdTAwMTg7XScwvfNpftZCMVZW+lx1MDAxMIpT8EhcdTAwMTmcjcd87Vx1MDAwNGON8eh9ROdcdTAwMDcn51dXXHUwMDFjfGA+3btcbkkvXXc8YmhcdTAwMDDGXHRcdTAwMTFiXHUwMDFhXHUwMDFlXHUwMDExpVx1MDAwNuBcdTAwMDTBlVx1MDAwMpLSSUBcbq55YkxcdTAwMDA8XHUwMDFlwlx1MDAxNHLOKV9cdTAwMDEy6061I6ftfzlcdTAwMWFcdTAwMWO+9c47g0/3XHUwMDFkeb69dbK+h/DFQefm9oi8Oz6Kqf3nXHUwMDAw9Vx1MDAxMXnHlmBcdTAwMTdd2Pt7u651LLZcYjz3vfc7wVV3XHR2l76880TD9Fx0XHUwMDFiekturtt+6rZcdTAwMDdcdTAwMWbIXHUwMDE3c0dYd+q47y9hXHUwMDE1tlx1MDAwZb+ddtPww5dTR4pcdTAwMDO307uEnzrN7DZcdTAwMTE5XHUwMDE4gVx1MDAxMjUrXHUwMDEyOYTNXHUwMDE2OZhcbkkolOWIeaRaXHUwMDFmXHUwMDE260qqrJZUXHUwMDA1MzhcdTAwMDTymclNPaeSKZyKSmHxyKVQICQpYCtIaJZcdTAwMTmI01RcdTAwMGVcdTAwMDIjrTWq5syKlVxuZilcdTAwMWE+Mn5pimaOXHUwMDFhXHUwMDE4VzSFj7WYe8D8XHUwMDE00HExXHUwMDEzc1x1MDAxMFx1MDAxMEq0lm1eUKg/ktZcdTAwMTNzXHUwMDE4XGJDUlx1MDAwMTiejjnIXHImZSZkiMyulSFcdTAwMGZcdTAwMThEslFwXHUwMDE3XHUwMDAwxMSQXHUwMDFjMcrQpKrRniGh4bhcdTAwMDBcdTAwMTJz71x1MDAxNkWiwIwvgsQkNeN021x0bCfo6s7yLNNotPrZvG1gXHUwMDAwrNVcdTAwMWGRmlxmqURcdTAwMDSI4raz2zOjbGNcckKylIdSJlx0YkRWRlxmS2x1XHUwMDE5wnBwcai2VGDPdVxuSE2/gOtcZkn/UV5ip/Bcblx1MDAxOTp7ytVnXlx1MDAwN4KYz3JrOswn3PLMJH1cdTAwMWL6vpPq5T9ccp0gXHUwMDFkX+Z8PbcyfPeUaY/36tuq9o1cdTAwMTNBlFlcdTAwMWNVsOW7zVx1MDAxMir5h+L951dTR7cnQzi7KsFbWtio/l8oXHUwMDA3g1x1MDAwMM1OwpD2g2KEmlx1MDAxN0VOXHUwMDBmXHUwMDA3b4OrvnQv/Y8n9uG9+5f7z81/y1061uaQXHUwMDE31OSFMIJcdTAwMDRcdMr1a4XNM1x1MDAwM4RcdTAwMTBDY4Q9dldy0v84XHUwMDE304QlKIa0dOhFUjHn6F03OtlNXHUwMDBmLv0t92T70N/12zPU9/+p2NPtrmh5l252XoY3fcKG3j4jw3skxdnHblx1MDAwNqfKT0ArysQkJuOtXHUwMDE1ZoX64GWoeXmrfvvWlVkhrmVWzlxmJiBmkIFVM2uzjFxmccZYRqsvmpA9OVx1MDAxZZ+XkO1pXHUwMDE1o+JcdTAwMTdOyOYog/GErPDxXHUwMDE50oaBurSMIy2NafNSiJfsXHUwMDFlmGfx9o06oJ3j+z3w7XhcdTAwMWKsO1x1MDAwMDFhXHUwMDA2gZzAXHUwMDFjX9mvXHUwMDFiY9KGXHUwMDFiWId8MWBdlI3kOkmQ1VrWiyib40+nZF/sWIdcdTAwMDNcdTAwMTd+ci/aN1F/XHUwMDAw/1c2y1I2K1ren8XsPME0fcKG3q60dI04rXLNilx1MDAwNFx1MDAxM6SYjTdcdTAwMTeEzTHQvoAnKKb6/VtXwqawlrC5NCjEWFx1MDAwZVx1MDAwNdVcblx0u2FcdDvLPzlcdTAwMDGlmy8jmJ5cdTAwMTiPz1x1MDAxM0y7YZi+uGCaozfGXHUwMDA1U+FjLfRmVrBcdTAwMTma/UicoEBcdTAwMTJcZnDzXHUwMDEydn32tq7QXHUwMDAzwFx1MDAxMFRcYplVQ1x1MDAwNVx1MDAxNmhcdTAwMDR5WEslwXV+wIfIw6uDXHUwMDFlXHUwMDAyhqSMS0klg1JCMYlEgVxyqZNIJFx1MDAxONY+M4nGgUmAhFx1MDAxNEm0XHUwMDAwMJ9R0F48k5ld0G5Q8C2PuWqlmVBcdTAwMDBcdTAwMDGlXHUwMDAy6pVgXGLDyqjH6jdFXHUwMDE0wmH2KTBcdTAwMWVcdTAwMGWYX89cdTAwMWXxqT61XHUwMDE59YkhoONcZnFJXHUwMDAw1uGEJnyCyOA6USZMZ8Ukq1x1MDAxM6BcdKd+qmr27GDOrokwLu1tVP8/mc8gwLNcdI3RXGbmXFw011x1MDAxMvXqal1cdFxyXHUwMDBiXHUwMDAzScFcdTAwMDVikmnJUFx1MDAxZVRcdTAwMGaEpqVcdTAwMDTCNPuVnFxiQenqXGJNYoNBiHQ8M4xw5cHeks6kgVx1MDAwNOeSUo6ZoHLy2V/B9Z2QhTLCZ/HZokJj2XxcdTAwMDZcZk1j2lx1MDAxYlx1MDAwMfR2MSRk+ZxiwVx1MDAxZNyA+TOTXHUwMDEwPGzognxWrzxGfKJASL1OTGtcdTAwMDRcbinhXHUwMDEzLlx0gzJ9XGZcdTAwMDGYXHUwMDFmm1iIn5rNZlx1MDAwNXJ2TYTwLCrbXHUwMDE4mm+ZUXSW6ngrtkKHtGNcdTAwMGbVaXmPrVx1MDAxYkfdbk95vP46vzLBl69mxkIqu9PvPzZ+/Fx1MDAwYlx0sVx1MDAwYuIifQ== ExampleApp()Screen()Header()Footer()

    Note

    We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.

    Both Header and Footer are children of the Screen object.

    To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:

    • textual.layout.Container For our top-level dialog.
    • textual.layout.Horizontal To arrange widgets left to right.
    • textual.widgets.Static For simple content.
    • textual.widgets.Button For a clickable button.
    dom3.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\n\nQUESTION = \"Do you want to learn about Textual CSS?\"\n\n\nclass ExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n        yield Container(\n            Static(QUESTION, classes=\"question\"),\n            Horizontal(\n                Button(\"Yes\", variant=\"success\"),\n                Button(\"No\", variant=\"error\"),\n                classes=\"buttons\",\n            ),\n            id=\"dialog\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.

    Here's the DOM created by the above code:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1d+1PiyFx1MDAxNv59/oop95e9VWO2+/T7Vt265Vx1MDAwYnVcdTAwMWSfOFx1MDAwYnrnllx1MDAxNSFcdTAwMDIjLyGIurX/+z1cdTAwMWSVXHUwMDA0Qlx1MDAwMihB2DvU7qhJ6Jx0n/Od7+tX/vz0+fOa/9j21v75ec17KLn1Wrnj9te+2OP3XqdbazXxXHUwMDE0XHUwMDA0f3dbvU4puLLq++3uP3/7reF2bj2/XXdLnnNf6/bcetfvlWstp9Rq/FbzvUb33/bfI7fh/avdapT9jlx1MDAxM95k3SvX/Fbn+V5e3Wt4Tb+Lpf9cdTAwMDf//vz5z+DfiHXlmttoNcvB5cGJiHlGjlx1MDAxZT1qNVx1MDAwM1NccuGCXHUwMDAzIzC4oNbdxpv5Xlx1MDAxOc/eoMFeeMZcdTAwMWVa29y+K3b2XHUwMDFlL5t5wXP7f0C+d212w7ve1Or1vP9Yf65cdTAwMDe3VO11vPBs1++0br1CrexX8TxcdTAwMWQ5Pvhet4VVXHUwMDEwfqvT6lWqTa/bXHUwMDFk+k6r7ZZq/iNcdTAwMWVcdTAwMTNkcNBtVoJcIsIjXHUwMDBmwVx1MDAwNeBQJkBcdTAwMTCquDBE8cHp4PvUYYRqo1x1MDAxOFBcdTAwMGVajtq11apjO6Bdv5DgXHUwMDEzWnbtlm4raF6zXHUwMDFjXqOhZGjkmfuvT6uZwzlcdTAwMDPA21x1MDAxOGZcYojBJVWvVqn69lx1MDAxYVx1MDAwMEdcdTAwMTMutVx1MDAxMs+3ipjiXHUwMDA1TUJcdTAwMDVQ4FLqsEmtXHUwMDAx7f1y4Fx1MDAxYv9cdTAwMWSt0qrbab9U3VrX/lx1MDAxMTHe2r0z6lhR54o0u5bnXHUwMDFi5sdlsVrq7JOTk8vCuVuQg7KGPNH3XHUwMDFl/LXBib++pFx1MDAxNXt4uv90kXso+p1tb72a71x1MDAxNlx1MDAxNfCb8cW6nU6rP225XHUwMDE5mTv3Yoeu/jLtXHLDYl9+XHUwMDBim73XLrvPwUulJFxco1trqfXgfL3WvMWTzV69XHUwMDFlXHUwMDFla5Vuw3j/XHUwMDE0sTeGMkN2RiGG6NGjr1x1MDAxMCOAXHUwMDE50IqEXHUwMDBlO1x0YtJreVkhRqVBXGZFXHUwMDA0XHUwMDAywoFTot6PMX7HbXbbblx1MDAwN1x1MDAwM3dcZs6oyThcdTAwMDNxXFxB41xmXHUwMDEwakK754Yr8/TO0Fx1MDAwYlpNP1978oKyXHUwMDFjQTUnIFx0KGOIUENX5dxGrf441LCBXHUwMDFio+VcdTAwMWLt9q//iFZ110NcdTAwMTOCMsXQxVx1MDAxYvVaxfr5Wlx0XHUwMDFmyutcZoWAX8OUPbigUSuX61x1MDAxMX8soVx1MDAwNS6W2dmfJn22OrVKrenWz4dcZkxccsmOV/KffXJMXFxcbkWS4pIqIyhorvTUgVx0rdO9XHUwMDFi/fitcX2+sd+oXHUwMDFlVcVV/2TZXHUwMDAzkyrHXHUwMDAwZ1x1MDAxNORzYFx1MDAwZcUloO8oYFxu/3/O/dmFpVx1MDAxOFx1MDAxM4daOVx1MDAxOKacKVx1MDAxMYtHrlxyIZpcbj3/eEzLcGfyxtvtq1x1MDAxZtWzwl3ha+OSi527+vLm+cLvf9z3v/Ltw69cdTAwMWRR3n2EXHUwMDFl8O2EhDxTuVAo7+/lbkuHeoPT80b9eKd5WZlDuVx1MDAxOVVvRsWq+9pe7m6HNjo7V+Vt/1x1MDAxZe6P6/OoXHUwMDA1Tjpt2rwvXHUwMDFkXHUwMDE1clx1MDAwN0e6sF1oXHUwMDFk+lx1MDAwN+8qd1x1MDAxMo9cdTAwMWFfQVOae/2jsXFSv5dU+OubZVM5OGo9PE5n7rLwMy7V6NFBXHUwMDFlkIgykis2fVx1MDAxZUh3tyXNXHUwMDAzKK1S8lx1MDAwMNdcdTAwMGXD9jCv/Cy7PMDH8TFcdTAwMTbDf6VBWDKTgc7Lmo9cdTAwMDFcdTAwMTk6msK/8qWO5zWTKJhcdTAwMWG6fm5cdTAwMTRsXHUwMDAyi1x1MDAxOaVgXHUwMDAzXHUwMDFiU1x1MDAwM+858MdEnoakwFx1MDAwM+CCXHUwMDEzXHUwMDFkYdyT4i49i35I3FEyMfCMcDTlhqlxgYcs1GFcXFx1MDAwZlximMgs8IjDjSQmyrVcdTAwMDbxx7hjXHUwMDE0SCEhTsRcdTAwMDQzmnEuYfZAXGasW3Qgdn2342/WmuVas4Inw3yGwVjq2fuuXHUwMDEzhzAqXHUwMDA1R3RRXHUwMDAyQZGE1W5cdTAwMWbPbdtmQ1x1MDAxMSlQR1xuIVxyXHUwMDA3yU3kipfeyDRF83LxILGuec3yRKOIUehPXG6lXHUwMDFj/idUqExcdTAwMDZWgYMyLyDMQe9cdTAwMThlKsms8VFcdTAwMWUzq+52/a1Wo1HzsfpPWrWmP1rNQX1u2PCuem559Cw+VvTcKFx1MDAwZbRticOkO/ztc1x1MDAxOCnBXHUwMDFmg9//+2Xs1etxXHUwMDE3tp+I84YlfIr+fJN0pJSz0cOv0MUlw3yKXjo1dDVcdTAwMGVcbnDyXGJ721vffpQ2L2BcdTAwMDOKXHUwMDA172Ohi09CLkaZI1x1MDAxOJJcdTAwMDFuOOJTRIpcdTAwMDVfZ9rRRFx1MDAxM2OoXHUwMDA0TcjSSEfJXHUwMDA1YGTDgnuIycPW9UFLXlx1MDAxZJT13f3ZUfuGnlx1MDAxZPKfynFeyjGj6l2tYucvSCdcdMfxXHUwMDBmXHUwMDEyXHUwMDE2+4qzXHUwMDFmLfAoiYDEXGJaY1wipVxiUtN3wKe33rKCtU5cdTAwMDNrRVx1MDAxY8OxNZShXHUwMDAxWC+BwFx1MDAwM6Q7iohcdTAwMGbocDczeOP7XHUwMDA03lx1MDAxZdJcIq+zYIE3gWuMXG68gY3v4Epa0KTo41x1MDAxMt2NSja9zDvYOqnkjqhqyZvzvavTi2qp+vWDx78mhlx1MDAxZlx1MDAxMlBkQ1x1MDAwNFx1MDAwNZ4kmqlcYvlcYr7OiaMkRiVjWiFtJ9nJvFx1MDAxOclcdTAwMTJcYoPijrxB3L2HK12RnD686lx1MDAxNVx1MDAwZp425FaDbPbORK/4kyvNiytlVL0/i/2QvvvxXHUwMDBmMjNcdTAwMDWbJem9jYIpZUZcdTAwMGZcdTAwMGYmQVx1MDAxMKlcdTAwMTjRfHpcdTAwMGWW3nxLmlx1MDAwNFx1MDAxOE1LXHUwMDAyXG5cdTAwMWNtXGZRQFnWSWA6XHUwMDBlXHUwMDA2XHUwMDA0oVx1MDAxZqniR3SyL46D5Votf+FcdTAwMWNsXHUwMDAyh1x1MDAxOeVgXHUwMDAzXHUwMDFiUyMvuZNd8sTIk8LYvs7pIy9dZC7p6JamXHUwMDBlhlx1MDAxNuGGcEKJNEORx4hxJDOCaFx1MDAwM1ooynl2kWdQaFx1MDAxOabBSKBcXI2LQ2rn63BGXHUwMDE4wlx1MDAwMGgqqVx1MDAxOY1LKvCo5lGVtrA+9zfFZXKf+1x1MDAxNH3SYcqLdoZzQTUxQiCY2s68cETyc9hDLzmAeVx1MDAxZLJk7OWCyX3uQ0alq6Vho7CtXHUwMDEwuLlcdTAwMTRcdTAwMWFcdTAwMTRmu7hNXHUwMDE0XHUwMDFjXHRCXGKtJeVcdTAwMTRcZo/ZtFJcdTAwMWTuid5sP3E/XHUwMDBly/tcdTAwMTT9OTOcof8ndr1TJrF6XHKfYT5lOmVbTkCTTDvAqMK4tLOhR1x1MDAwMY1Sh1wi2+DUgLQ/Mlx1MDAwNDTJXHUwMDFkaWPMoFSkhof9SpFZ29QhdpYrXHUwMDA1jf9Hc81gNpfSXGZcdTAwMTBkVlx1MDAxZtBe0YA41FBlQYrhXHUwMDAzXHUwMDAzeiRcdTAwMGKD41x1MDAwNVxyXHUwMDE0Nlx1MDAxMuproGiMXHUwMDE2KP4jV7xlsG7SXHUwMDE4XCJxbIojXHUwMDFjMHtcYkFcdTAwMDVXMZO0g4RcdTAwMDBcdTAwMTDFlE2ETOskk8ZcdTAwMTOYlYazRFdcdTAwMGVOxpx4XmiGzZ9cdTAwMDRmKFx1MDAwNdArpJl+XHUwMDFjMX0u1pJOgUA1ZFeYIEkzXFxhK1xmz1xyt8OMhklCMckwxSC7rmnpXHUwMDE4XHUwMDE0XtyuMEF6xWSYZFwi5Fxmg1xiXHUwMDA1kUJYZaBRxsWwjGDcc6pM+IyrimVvJmdAQFAk2kZgXHUwMDE2UiQ+e8Jgi2rkb1x1MDAxYUOKMKNeqcGM3Kx0ly/+vv/Acr9fX5y6hzfXh+R4N8EmgsmQXCJhMVx1MDAwMVx1MDAxZFHAY0ZR7mhO0dHAXHUwMDAwIKc0sNJwtp7ozvZcdTAwMTN35Fx1MDAxOfEstcNcdTAwMWaISOzroUhJXHUwMDE4g1kmdqW385KiXHUwMDFh5n5cdTAwMDdcdTAwMTBcbjhcdTAwMTOgsFx1MDAxMUKkeJ5cdTAwMWVhXHUwMDFjhjrBKNQoQI1cdTAwMWGxa55cZi1EzLDHP0wpr7jFgGk0KFSkXHUwMDBi6ep/8vvC2z2R3fLvKt8/qNxvbVx1MDAxZW/+7OqfV1d/RtW7WsVmNU9/tWrhXHUwMDFk0/RcdTAwMTPKnTQyMf5BpjT3x7fqU1x1MDAxOfLFYt4tnpeqV4WN+lnCmo2ZXHUwMDFh7eBcXO+c9HKHXHUwMDA3Z113vXrQudl4qO9OV+5rXpwjXHUwMDBiS02xyatJVeJyXHUwMDA14NoopF7TS4Z0d1vW5FxuLC25XG6kcotJrmJMco0ssHxNrlx1MDAxMrOrnbKbQXbNeiSFyqGjKSMpW69jXHUwMDFjv35v2lx1MDAxM7Xyv77bjVx1MDAxN+qtyve1783xIyxcdTAwMDKGylx1MDAxOVxmoNS9m2Hnn2l8ZVx1MDAwMmNcdTAwMWNcdTAwMWRfmWj5O6gwJ4mDL+jAXHUwMDAwis6wvcTR3vXm7Teyv5477uzU1pv89NvjzrJcdTAwMDer5Kg17LwzrpRcclZcdTAwMThcblZhuGNQh2CsZlx1MDAxZKxcdTAwMTFNXHUwMDFlMuF4b6TQilx0XHUwMDEzXHUwMDFkgF1cYlx1MDAxNW7vNftcdTAwMTe3x9c7ZVLNre+vXHUwMDFmk3rx8CdcdTAwMTWeXHUwMDE3XHUwMDE1zqh6V6vYrKjwatXC/KlwRuZOYtjjb5g9XHUwMDEzTi13v3/WKPTFjr7eJFx1MDAxN/6RPKrRq4TJ7TOVu9voXHUwMDFll76dNd2LgtgvbNx5lav1/nTlLlxyc+cscVwirFCEK1x1MDAxMd1cdTAwMTllXHUwMDEyXHUwMDE5SHe3pSVcdTAwMDNcIoVcZkhiXHUwMDFjvlx1MDAxODKgx5CBMczdjklcdTAwMTi7XHUwMDA2/O/M3PeQXHUwMDEwP1lcdTAwMGVcXP/VXHUwMDFlfybBpbrb7XpdZMLXPd9vNbuLJvFcdTAwMTPIbmyi+lxmXHUwMDBm8Vx1MDAwZT4vk/eM4VRwXCJcdTAwMDUjU4dweX/f39l+2uHXrFuu7l+oRv/SXfZcdTAwMTBcdTAwMTZ2N1x1MDAwMCUkl1JcdTAwMDCG8PB4nZLS4YBS1273xIBnuJXTlHzeLmoxoFx1MDAxNzyJfbt597he8lxi9b8+Ulwi+24+d978SefnReczqt7VKjYrOr9atZBcdTAwMTWdX61aqJvNLX29u8W3pPTy51x1MDAwN1x1MDAwZpVcXG5KevxG9TH+Qf6P2Hx0ksdo11x1MDAxZWFMaVxyYno6n+5cdTAwMTfLylx1MDAwNVx1MDAwNEvjXHUwMDAyii6KXHUwMDBijKPzKsZcdTAwMDVcZppIuFAruKp0eja/XHUwMDE5MN1cYlx0/r524SHx/fL8173bqblNXHUwMDFmKXG3Vyrh0yXzejVcXPiceP1cdTAwMDTSO8rr3/Y472D4KD6TwtpQ6+UgZpiR97Tlnp6ap7PKZvGylds4h0b+dNmjWlx1MDAxOe1IXHUwMDAzxE5cdTAwMDWFmEhXUjh2wi5+1FIwfCCUMY46fcG7QbKvxW5+s974arbyxe5cdTAwMWb8x5Ms/tzTY25cdTAwMTQ/o+pdrWKzovirVVx1MDAwYllR/NWqhflT/IzMnaRcdTAwMWPG33BKa98xvvDy28crh2h/cmxcdTAwMDVcdTAwMTNcdTAwMDXgTNLplUN6+y0px9CEpXFcZkVcdTAwMTbFMaZTXHUwMDBlVFx1MDAxOY5qLjqz6v9DOlx1MDAxY7XGUG1cdTAwMGZcdTAwMDOrs2jdMIFKT6FcdTAwMWImPUtqMKeLXHUwMDA2XHUwMDAxyX1cdTAwMDFcdTAwMDJQz3NcdTAwMDPTr0ms9o+L9fouK/hf20y15GX+zr1e9ohmmjtcdTAwMTiqWmKYXHUwMDA0fVx1MDAwMWIopIWRjkBtpbnKXFw2jFm6M2ZgwFx1MDAxMIFBRt+yd+l7ZMN957Kfq2z+qKpK+3ZrY+Obq9tnP2XDvGRDRtW7WsVmJVx1MDAxYlarXHUwMDE2spJccqtSXHUwMDBik3j4+Fx1MDAxYi4hX1x1MDAxNjqRL2thqN1cdTAwMGV++uSaXs1Lm1xcTVpylYQuKrnqMcl1XGZfJswgX1ZvWFx1MDAwNrs6dDnvu8hgXyaNn37byZ/vXHUwMDFmXHUwMDFmfbF/jM4+uet5Xb+WOokmXHUwMDFi0jyBSca283/rXHUwMDEzpcZ14uJ3mVx1MDAxONacXHUwMDFhLYDNsJAlfcXQkoZ1sP0/U6hcdTAwMTCQjGL0XHUwMDBl78sqXHUwMDE4c4BJxlXmKlx1MDAxOIyDXHUwMDE1zijeXGLsmmFcdTAwMTVcdTAwMGZyyZ3xu0RyorRcdTAwMWHaROpvseR9luXlQjBit8IhWIfcaFx1MDAxYbnqdTtcIq00XHUwMDExxK7GJnY19stcdTAwMDVcdEveh1x1MDAxZmOVlp0ne5L9XGZ8KCznU/Tn7JtcdTAwMDFFXHUwMDEy/ygz0NpcbrxcdTAwMTmW16RPiV5SXGKRXHUwMDAwXHUwMDBlXHUwMDE3WNtCWM+iw3tcdTAwMDFcdCZcdTAwMWNcdEoxXHUwMDA1zDBcdTAwMDZmxK45QlxikY7dt9A6ONGM6DFcdTAwMTBcIrD57Yt77IxcdTAwMDBjKJD4XHUwMDBiRYDYxXJcdTAwMWM+4IVcIktcdTAwMDEmxOFKcFCSSqVcdTAwMTFRTPw1XHUwMDFm2lx1MDAwMWk4J4Tad3wgXHUwMDE3TMeSJJPS59eOmKSEXCJUMlwiqNAmvpdcdTAwMTF37I5mTHGBXHUwMDE2XHUwMDBiyVTMpFVcdTAwMDKxZFe2n7hcdTAwMTPPXHUwMDBizCRJ3jaDM/vuVD3DsED6QMmSoplcdTAwMTLSXHUwMDAxu1x1MDAxZj/XytjN3IZljlx1MDAwMcegrmBcdTAwMTK4nYmhR+ya42ZAxNFcdTAwMDKBk1x1MDAwMlD7hoKw2kM+JFx1MDAxY5RiRiq7ZT5nXHUwMDEw31ODW9/h5i1vjV1mLJtcdTAwMDE4wM7+4kiJhGG2VaPvSVx1MDAxYeyJSKyu5VxiL1x1MDAwNONN67ehWfrowDBbXHUwMDAzKkBSXHUwMDEwOliCQlh8IzTlIFx1MDAwMCuqXHUwMDA3RGU1gSzRi4OTo/47L1x1MDAxOItsZVx1MDAxN+NklGKiYDO8TTF9ouiyolx1MDAxOJNcdTAwMGV7fte1XHUwMDA0VFx1MDAwNsNbPVtcdTAwMTSzkk4pad/LrNiIXfNDMbxcdTAwMTFcYlRnRnOmkVSNQzHlgFx1MDAwNo2pXHUwMDA1qN2bKbZcdTAwMDSKoZnYZvxvRsimXHUwMDA2seC9YkhcdTAwMDFcdTAwMDBZqaaMKjlmL0dKXHUwMDFkgdyIXGIuUPNgXHUwMDEye+N2s+lzI0esYsq+XCJcdTAwMDVcdTAwMThcdTAwMTNKo9aKb2kmXHUwMDFkXGZuJIlcdTAwMDSI3Ss0btMqYdl6ojPbT8yNk8Ds08tcctbcdtv2dnmD1kC/rpVf+lx1MDAwMMOnXFy7r3n9zXjc/XJcdTAwMTN8bMdXUJ9cdTAwMTaJPPusf/716a//XHUwMDAxsk3fXHUwMDAxIn0= App()Screen()Header()Footer()Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")Static( QUESTION, classes=\"questions\")

    Here's the output from this example:

    ExampleApp \u2b58ExampleApp Do\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    You may recognize some elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.

    "},{"location":"guide/CSS/#css-files","title":"CSS files","text":"

    To add a stylesheet set the CSS_PATH classvar to a relative path:

    Note

    Textual CSS files are typically given the extension .tcss to differentiate them from browser CSS (.css).

    dom4.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\n\nQUESTION = \"Do you want to learn about Textual CSS?\"\n\n\nclass ExampleApp(App):\n    CSS_PATH = \"dom4.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n        yield Container(\n            Static(QUESTION, classes=\"question\"),\n            Horizontal(\n                Button(\"Yes\", variant=\"success\"),\n                Button(\"No\", variant=\"error\"),\n                classes=\"buttons\",\n            ),\n            id=\"dialog\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    You may have noticed that some constructors have additional keyword arguments: id and classes. These are used by the CSS to identify parts of the DOM. We will cover these in the next section.

    Here's the CSS file we are applying:

    dom4.tcss
    /* The top level dialog (a Container) */\n#dialog {\n    height: 100%;\n    margin: 4 8;\n    background: $panel;\n    color: $text;\n    border: tall $background;\n    padding: 1 2;\n}\n\n/* The button class */\nButton {\n    width: 1fr;\n}\n\n/* Matches the question text */\n.question {\n    text-style: bold;\n    height: 100%;\n    content-align: center middle;\n}\n\n/* Matches the button container */\n.buttons {\n    width: 100%;\n    height: auto;\n    dock: bottom;\n}\n

    The CSS contains a number of rule sets with a selector and a list of rules. You can also add comments with text between /* and */ which will be ignored by Textual. Add comments to leave yourself reminders or to temporarily disable selectors.

    With the CSS in place, the output looks very different:

    ExampleApp \u2b58ExampleApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aDo\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS?\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aYesNo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/CSS/#using-multiple-css-files","title":"Using multiple CSS files","text":"

    You can also set the CSS_PATH class variable to a list of paths. Textual will combine the rules from all of the supplied paths.

    "},{"location":"guide/CSS/#why-css","title":"Why CSS?","text":"

    It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your .py files?

    A major advantage of CSS is that it separates how your app looks from how it works. Setting styles in Python can generate a lot of spaghetti code which can make it hard to see the important logic in your application.

    A second advantage of CSS is that you can customize builtin and third-party widgets just as easily as you can your own app or widgets.

    Finally, Textual CSS allows you to live edit the styles in your app. If you run your application with the following command, any changes you make to the CSS file will be instantly updated in the terminal:

    textual run my_app.py --dev\n

    Being able to iterate on the design without restarting the application makes it easier and faster to design beautiful interfaces.

    "},{"location":"guide/CSS/#selectors","title":"Selectors","text":"

    A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.

    Selectors can target a kind of widget or a very specific widget. For instance, you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.

    Let's look at the selectors supported by Textual CSS.

    "},{"location":"guide/CSS/#type-selector","title":"Type selector","text":"

    The type selector matches the name of the (Python) class. For example, the following widget can be matched with a Button selector:

    from textual.widgets import Static\n\nclass Button(Static):\n    pass\n

    The following rule applies a border to this widget:

    Button {\n  border: solid blue;\n}\n

    The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Button Python class extends Static.

    Static {\n  background: blue;\n  border: rounded white;\n}\n

    Note

    The fact that the type selector matches base classes is a departure from browser CSS which doesn't have the same concept.

    You may have noticed that the border rule exists in both Static and Button. When this happens, Textual will use the most recently defined sub-class within a list of bases. So Button wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, the buttons would be \"solid blue\" and not \"rounded white\".

    "},{"location":"guide/CSS/#id-selector","title":"ID selector","text":"

    Every Widget can have a single id attribute, which is set via the constructor. The ID should be unique to its container.

    Here's an example of a widget with an ID:

    yield Button(id=\"next\")\n

    You can match an ID with a selector starting with a hash (#). Here is how you might draw a red outline around the above button:

    #next {\n  outline: red;\n}\n

    A Widget's id attribute can not be changed after the Widget has been constructed.

    "},{"location":"guide/CSS/#class-name-selector","title":"Class-name selector","text":"

    Every widget can have a number of class names applied. The term \"class\" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag will share styles.

    CSS classes are set via the widget's classes parameter in the constructor. Here's an example:

    yield Button(classes=\"success\")\n

    This button will have a single class called \"success\" which we could target via CSS to make the button a particular color.

    You may also set multiple classes separated by spaces. For instance, here is a button with both an error class and a disabled class:

    yield Button(classes=\"error disabled\")\n

    To match a Widget with a given class in CSS you can precede the class name with a dot (.). Here's a rule with a class selector to match the \"success\" class name:

    .success {\n  background: green;\n  color: white;\n}\n

    Note

    You can apply a class name to any widget, which means that widgets of different types could share classes.

    Class name selectors may be chained together by appending another full stop and class name. The selector will match a widget that has all of the class names set. For instance, the following sets a red background on widgets that have both error and disabled class names.

    .error.disabled {\n  background: darkred;\n}\n

    Unlike the id attribute, a widget's classes can be changed after the widget was created. Adding and removing CSS classes is the recommended way of changing the display while your app is running. There are a few methods you can use to manage CSS classes.

    • add_class() Adds one or more classes to a widget.
    • remove_class() Removes class name(s) from a widget.
    • toggle_class() Removes a class name if it is present, or adds the name if it's not already present.
    • has_class() Checks if one or more classes are set on a widget.
    • classes Is a frozen set of the class(es) set on a widget.
    "},{"location":"guide/CSS/#universal-selector","title":"Universal selector","text":"

    The universal selector is denoted by an asterisk and will match all widgets.

    For example, the following will draw a red outline around all widgets:

    * {\n  outline: solid red;\n}\n
    "},{"location":"guide/CSS/#pseudo-classes","title":"Pseudo classes","text":"

    Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the :hover pseudo selector.

    Button:hover {\n  background: green;\n}\n

    The background: green is only applied to the Button underneath the mouse cursor. When you move the cursor away from the button it will return to its previous background color.

    Here are some other pseudo classes:

    • :disabled Matches widgets which are in a disabled state.
    • :enabled Matches widgets which are in an enabled state.
    • :focus Matches widgets which have input focus.
    • :blur Matches widgets which do not have input focus.
    • :focus-within Matches widgets with a focused child widget.
    • :dark Matches widgets in dark mode (where App.dark == True).
    • :light Matches widgets in dark mode (where App.dark == False).
    "},{"location":"guide/CSS/#combinators","title":"Combinators","text":"

    More sophisticated selectors can be created by combining simple selectors. The logic used to combine selectors is know as a combinator.

    "},{"location":"guide/CSS/#descendant-combinator","title":"Descendant combinator","text":"

    If you separate two selectors with a space it will match widgets with the second selector that have an ancestor that matches the first selector.

    Here's a section of DOM to illustrate this combinator:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSFx1MDAxMn7vX+HwPOzsRKOp+5iIiVxyfODbbjdu293bXHUwMDEzXHUwMDBljGSQXHUwMDExXHUwMDEylmSwPTH/favARlx1MDAwMiRZXHUwMDFjYmCnefAhQSmVyuP7KiuLPz9sbGyGz1x1MDAxZGvzt41N66lec2zTr/U2P+rjXctcdTAwMGZsz1WnUP//wHv06/13NsOwXHUwMDEz/Pbrr+2a37LCjlOrW0bXXHUwMDBlXHUwMDFla05cdTAwMTA+mrZn1L32r3ZotYP/6J+ntbb1e8drm6FvRFx1MDAxNylZplx1MDAxZHr+4FqWY7UtN1xm1Oj/Vf9vbPzZ/1x1MDAxOZPOt+phzW04Vv9cdTAwMDP9UzFcdTAwMDFcdTAwMTEg44dPPbcvLVx1MDAxNFx1MDAxY1x1MDAwYimAkMN32MGOumBomer0nVx1MDAxMtqKzpj9a1x1MDAwNVx1MDAwN7x5b1Pil1x1MDAxOWx/2nX2v5Ur0XXvbMephs/OQFx1MDAxN7V689GPSVx1MDAxNYS+17KubDNs6quPXHUwMDFkXHUwMDFmfi7wlFx1MDAxYaJP+d5jo+laQTDyXHUwMDE5r1Or2+GzPlx1MDAwNsDw6EBccr9tREee1H+UY4MxTlx1MDAwNGFcdTAwMDBxQfDwrP48ZtJcdTAwMDBEQCoxoOokXHUwMDE5k2vbc9SzUHL9ZDHOXHUwMDAxiCS7rdVbXHIlnmtcdTAwMGXfXHUwMDEz+jU36NR89cSi9/Xe7pix4bGmZTeaoTooRHQ9q693iFx1MDAwMFaCMFx1MDAxYd2FvkrnwOxcdTAwMWLBXHUwMDFm43pr1vzOq342XHUwMDAz/U9MQi3c7rhcdTAwMDXFrSj2bKl7V7rCsve1aVx1MDAxZcKb26dcdTAwMTPGYDBcdTAwMWNrxORC6yncXHUwMDFjnvjrY9aw7W9fji727ra64ZfLw9557+Wk0blNXHUwMDFltub7Xi/vuFeHl93eMdk5OfapufeMXHUwMDFlXHUwMDEx2WFcdTAwMGJcdTAwMThcdTAwMTddmVx1MDAwN/uVVv1ElFx0vGg7Z7vut8ZcdTAwMDLGLUi96zXs8ePOPpDXXHUwMDA3L0934qhSMZ9uat3eXHUwMDBm5S5m2H1rXHUwMDBmXHUwMDA33bpdvtm+XHUwMDBi7ytcdTAwMDe75fOr47mUO1wixce8N5JT3Obxdlx1MDAwZmBcdTAwMGK07zudg6Pj7svN+U6YT9zXv6JQ+Ngxa4OspYIsJJBcIiZcdTAwMDSPwq1juy110n10nOiYV29Fie5DTOCJXHUwMDE0O3L/I9lcdTAwMTWK8cNv2ZVzKiHjMMpZ7yXXbLNY2eQqspKrIFx1MDAwNl1OcqVcdMmV8vHkilR2RVx1MDAxY9LosS0sty7SXHUwMDFho4fuuWHVfunDNTZytFJr287zyHPrm6mSdFudrtmu5f/83dUnbPP375umXXO8xvfN7+6/41pcdTAwMGUsJY1cdTAwMWWeopFxyo7d0Ca+6Vh3o7ZcdTAwMWbaXG6oXHUwMDBlT7dt04xDz/rbtVx1MDAwZvJcdTAwMDBGz7dcdTAwMWK2W3Mu8kqe6abZSJhClOarkCtcdTAwMTAsOVx1MDAxMjy3s55fPDjWS3nr8XHr5MA5Obt2ryv3f6+z0nd9lVx1MDAwMYNCXHRcYodcdL5KMTOkgJwqV9W+SlN9XHUwMDE19F9z+KpcdTAwMDSTvirYhK9cdTAwMDKAMYNAXHUwMDE24KxZiem+etjYvfDc7Va9xfyn1sG31kNKYvpcdTAwMDGEp1x1MDAxZrcg9a7XsEVcdTAwMDHh9dLChd/c6d12rYtcdTAwMTBcdTAwMWY6XHUwMDEwiZfWeWc+xLqOWlg8blx1MDAxZoyLn1x1MDAxZkpfkajUQ9JcdKrW887Z9lx1MDAwM1/AuGZcdTAwMWJcXG57h597leujbafb8XdvXHUwMDFiuFCekaz4aNjXvzJcdTAwMTFcdTAwMThmJMr/RfFcdTAwMDGKU/lcdTAwMDCkilx1MDAwYlxiRkGUV9/DXHUwMDE42fa2qlx1MDAxOINmYVxmXG5ccrJcdTAwMWOMIVx1MDAxMjDGJFx1MDAxZpCAXHUwMDEzIFxiL2CubZHWOFx1MDAxZlx1MDAxZthXMPtFI2vnZ318XHUwMDAwretOLVxirEDh69vHMPTcYNnU4Fx1MDAxZFx1MDAwND1OXHKmuYlM581mXHRcXKbOlzPMXHUwMDA1Izj/bPmX8PzCe3q+2eptnZ1+uXj5/PKpzFL8d8xcdTAwMGb/LjpPODGApFxiXG7JuXJfQkb8lyNqKCpcdTAwMGJcdFx1MDAxM8rJXHRcdTAwMTfz+O9PXHUwMDEw3VxuoajxzFxmXHUwMDAxXHUwMDEyrKRcdTAwMDScocW7b1ZcdTAwMDZkrOneyy77elolnYY8wkfHj5X5gcBcdTAwMGaGUKh612vYolx1MDAxOMJ6aaEohrBeWnDk1ra43dsm24xZ1Yujp0alssK2sHyCkHwjOcWtXHUwMDFmnV6QzrF/+tXfvjWt44483vuyuELEUoiHiNWWx4lcdTAwMDeTXHUwMDEwXCKJRX7ikW1cdTAwMTcrWokgXFxmQlx1MDAxNypccrwo6DI99YBcdTAwMTFsfJvepLpARPCyS1x1MDAxMUulXHUwMDFlW31Y/vP3za+WwubJ9Fx1MDAwMpKRj1xy+UNd3Y7lz04w3oHf41x1MDAwNGNcXNRMR8wmXHUwMDExXCK2pGbMXHUwMDFiXHUwMDExXHUwMDA0XGZDOUVZkMPKWYvsud3mXvUlaODnXHUwMDEz379ebVx1MDAxNsEkMqBSXHUwMDAye3XF0UlcdTAwMDBNXCKo1GW41SBcdTAwMTFAeyElYMkk4iGQlaP2WZVcdTAwMWOet2jFNe/RzqfLXHUwMDFmJGJRJKIg9a7XsEWRiPXSQlEkYr20UFx1MDAxNIlYLy0svijyXHUwMDFlN0m+kWjY17/+flx1MDAwZSFlOodAurTBXHUwMDEwyT/5mf38VpRDMMmzgFx1MDAwYtVcZmNBwGVcdTAwMTFcdTAwMTSCS0VcdTAwMWZcYohhmv9nXG5x6i2bQbxcdTAwMDO9U1x1MDAxOcRA0kwvXHUwMDFjXHUwMDA0l1x1MDAwNDdkIL2EXGIh1oYp83thdm15NUuITFx1MDAxN1x0XHRjXHUwMDE0YFwigEQjPkhcdTAwMThVJ1x1MDAxMZVcdTAwMTJDKinjhfkgMDBcdTAwMDZUcixcdTAwMTCmXGIozdNJn2TSgMopKIVCxVx1MDAwZS7wuItcIlx1MDAwMDliKFZVyu2ifVlndVHC4UxcdTAwMGJcdTAwMGWDsOaHW7Zr2m5DnYwy3VsnSp51fX2nrj9cdTAwMDZ9LVx1MDAwMkY5xVxcqVx1MDAxMFx0XHUwMDE5X1x1MDAwM6p1UetoTmZgSVxiXHUwMDAxQFExiFV4fX3DMONuWq75vkzZXHUwMDA1xZhMJSVcdTAwMTTmXHUwMDAwqStcIlxuoZIv8rmhUMjAnDMpKEVcdTAwMTLolDAhlFNcdTAwMGLCba/dtkOl+0+e7YbjOu4rs6w9vWnVzPGz6qbi58ZDQkePOMpcIqO/Nlwin+n/M/z7j4/J7043Zv2aMONovFx1MDAwZvHfU0czXGJcdTAwMDFcdTAwMWU/PKynXHUwMDAySFx1MDAwNIQyesN70SxcdTAwMWK8rSqmYMTgXHUwMDAyMimFJJLEVozrzzNMXGYqMUOIUPVcdTAwMDY2LtdcdTAwMDIxXHUwMDA1xFx1MDAwNkZQXHUwMDFiPEbKolGk92h2XHUwMDA0XHUwMDE4XGZzhFx0xFx1MDAwNKugXHUwMDE1W58xXGJnXHUwMDAyS6DcgswwVzJXOJtcdTAwMTVx5FxmZ7lDXHUwMDA3MFxiplx1MDAxOCCgMCDAkkNcdTAwMWPzo9fIXHUwMDAxocH0Ulx1MDAxZklcdTAwMDBcdTAwMDE6XHUwMDE0z1x1MDAxNs6ywceoTFxiS8l1L1x1MDAxZmBcdTAwMDKDJJlUXHUwMDAwoFKrknKOXHUwMDEw4utcdTAwMWTO0m1ZvyaseFHRLD5tO4HNJCfKmfNcdTAwMDez7CrZqlx1MDAwNjMsXGaKXHUwMDEwg0JiXHUwMDE1zMZjXHUwMDE5M1x1MDAwMMVcdTAwMWF8MJX1i1x1MDAwYmVSKmvGkimszFWWTmr9XHUwMDEwKuhcIqFbXFxcdTAwMTGEKn5MrPyCXG6VIKpS/z81lJU0JiCSqidJmYLZQrCYXHUwMDE3vcVccmyoUKf8jFx1MDAwM4XFlabfkMGUwSy7XHUwMDE2MyqVTodSuVx1MDAxM0QqpElcdTAwMDQnhKJcdTAwMDZcdTAwMDRUhVfOgZZrUqR1XG5lpVRj1q9cdDOeMpRl1qlcdTAwMTQySVxyZyq39d08f9HYadbA2XFnL7Shf0rK7Z3wyjxdcapJdEfMSFxmQ0hcdTAwMThQQlx1MDAwMlx1MDAxOCpwcSpNoJKCXHUwMDFiiKtcdTAwMDTCJ0BcdTAwMTfGkGDFhJfcXHUwMDEw7jlP1XLrwobynpZcdTAwMWN0ePxcdTAwMTLUq/PPwJ6cXHUwMDFmvHytPF2H/o5ValaDa47I3T+wQFWQelx1MDAwYlx1MDAxYZZ37f3Kwy5s+7s35k7YRd0zZ1x1MDAxMVogwO9At1s/vapcdTAwMWOdiqudK+8kPFpd7d7et8ufnC6DNCxtmbJxdKqyWqHlg+RcdTAwMWLJKe5cdTAwMWM91pnjfjmzzmXpwWqJ3a8v95/cs8vSXU5reEtb70CkXGJHXHUwMDE3VO6gNH2elWFFXHUwMDFmXHUwMDE0ssjfXHUwMDBlmm1uq5r86Hjyo9JcdTAwMTBcbm5cdTAwMTSZ+khC6kNcdTAwMTPTpopaKFx1MDAxNlFExluk4UXPNypsIDByNKOwUa37luX+nFLS4CPvX1hJ41x1MDAxZJQ2XtJcdTAwMTjKmOljqYSZp1ZcdTAwMTSFXHUwMDAy+EDPUud2sexYtpouRlx0MahmWFx1MDAxYzLEYUxcdTAwMWT93Vx1MDAxMaA0KKSFXCJNXGJcciggZlSRKSooTZj0IypcZqQgT6hcdTAwMWVRX+5cdTAwMTkqjPOWL/gsjpiTJGd7wUZ8bo1KrtNcdTAwMDElkjCMXHUwMDEyKDIxXHUwMDAwXHUwMDA1XFxcdTAwMDfN1zNTUuMpSilMPVx1MDAwZu04XHUwMDA0ci6jRqS4KEA339F+c1x1MDAxZJZyQqJ1YsbptqtfMauNXHUwMDA2+lx1MDAxMP8929pNlE6KXHTlXGIjPsVcdTAwMWPf+YV74MnHo93OTfPo7vzm0uJcdTAwMGbWiscsjJShgdGwNNgoTShwQFx1MDAxMFx1MDAxNJz2o1x1MDAxMlx1MDAxYpNogVEr305pSGFcdTAwMDTlnXyG2upcXFx1MDAxYqV99tF1q/n5oXlwWeru032zu3OfXGZ+fyzcnH7cgtS7XsNcdTAwMTa2UdpaaaGgYYvacKEgcVx1MDAxN0/i393XLfFGcor7cIvLVVRmdyc7XHUwMDE31WvvM7l7MlvrNTeAkEhdtVx1MDAwMLFUp5mg+SdcdTAwMDey7WJVUVx1MDAwME1GXHUwMDAxglx1MDAxOHhJKCDflm5QXGJcdTAwMDSgXHUwMDEyqID+jaInXHUwMDBi5tzTLbBN67bmL3/nhkxUm2tTt1x1MDAxMdHngOtZJXmqWFx1MDAxOVx1MDAwNGCKXd26veolsvDR4fVpq1x0fdn4dmydpHhq3feCoNSshfXm3++tXHUwMDEwXHUwMDE5irlob1x1MDAwNWNe2f88YobgXHUwMDA1zjJwPumpXHSdVlxiXCLMgIRL3tCtVVx1MDAwNtf7N4dP5llcdTAwMGaf7l1cdTAwMTDzmJXdXHUwMDFmgH1RgL0g9a7XsEVcdTAwMDH29dJCUZ1W66WFojqtXG5cdTAwMTJ38ds1XHUwMDE0JO57tCX5gjmlnYO2ZI5bpdVnSlx1MDAwZt2DXHUwMDFitoPuXHUwMDFliLtXPkwhhStLh6RM7WdXUiBAOEf5XHUwMDBiOdl2saJ0XGLyTIBFoIGLXHUwMDA0WCxcdTAwMDFgTVIhLplUkFx1MDAxN69h2XTqfjB9bMAnvm9cdTAwMWW4QVhznGXzoHfYQkp7WKrgmb6Z3i+W3rQpOVx1MDAwMphROMWOdZnrOlbTNSlcdTAwMTaGXHUwMDE4W7XwVl3tN1x1MDAxY1x1MDAxNemXRK/PXHUwMDFjxIVJXHUwMDA3JdKgjI0tJVx1MDAxY+5Dj1VUXHUwMDE1s1CheVdcdTAwMWbP5KqLLqyWgMGERIxJXHUwMDE1WpmEOKr3bkTlTIIxlVGFL6WyOir/XHUwMDFhVThLSfajXzHLicb4XHUwMDEw/z11nIgtZlx1MDAxYe/DYlxcb6RI85c1s7HSaoZcdFx1MDAwMqhBqMBcdTAwMTKqZIpZbHnBoK2UXHUwMDE4sexe5LQm0F83JaTuXHUwMDFml0xFZ0FcdTAwMTJyO4OGnmIl8vVcdTAwMTX74ozXuVx1MDAxNEgp5ZLPsjn+Klx1MDAwN5DsucXRXHUwMDAwotuwXHUwMDAwJZjplUSQxL4oYFx1MDAxOEK4IbBKglx1MDAxMFxyNJnQK5BrjUZ2pt9cdTAwMThpd1VcdTAwMDJJSlx0XHUwMDEyglx1MDAwYoZcdTAwMTPaXSfbW9cpZmWYr35NXHUwMDE47pTxK70kk1qRwSp5UEbzdypcdTAwMWP4n+Wlc/FcYus9XHUwMDFmXHUwMDA1l1x1MDAwN95J2fs8+ywvXHUwMDFh97VcdTAwMWMhLIpN03RfIVx1MDAwMlxmoWyLxSdz+ztTXGJmXGIwWIU0T+D66a5GXHUwMDExRYm7aaHoPqOFm5MrxpBe76ZcdTAwMDRZ8qKM4jdxXFzewtC2traNsGlcdTAwMDVWXCKbiYHGadhM6HXSqMzIrYzzlrg4s2FcdTAwMGYq0796R1LdcVx1MDAxZW98fs99s1x1MDAxZvVS3He25knMXHUwMDEzvLTvvlx1MDAxMlx1MDAxOFhcbiwwXHUwMDAzXHUwMDFjXHUwMDAzmr6pRY6vycrwYa5cdTAwMThcdTAwMTFcdTAwMWWjRNH2MsRQaVx1MDAxMlx1MDAwMVxyyrnKp5N7WWi6XCJcdTAwMTBe9lZcdTAwMTZcdTAwMDUjjuxsMJLbXHUwMDExXHUwMDAwXHUwMDEyXHUwMDEzhiBVhEVSjmLvXHUwMDFh9ktKQvHMi0Fz90lqYaR6XoLrfnxAJUlo3uRcbvFcdTAwMTJFoF43ROVcdTAwMTMyrVx1MDAxM/BIMF79Kk3a7YIgXHUwMDA3lOlcdTAwMWRcIpwjpU+FP3NHLeubXHUwMDBm7ZOtW8e8XHUwMDExqGe13Fx1MDAxNv+yv1x1MDAxNqBcdTAwMDNcdTAwMTNcdTAwMTW1JjGHOmAw2bf1oiCHiEJNXHUwMDA25GBSXHUwMDExOimW/SVcdTAwMDCrNbE/XHUwMDFm4vjF9Nx/hb9svKV6O1hcdTAwMDXgkSDVbPhcdTAwMDPFZsTH9+NcdTAwMDVUXHUwMDA1T8R5fk/OfvCrjD9cdTAwMTAxMMaC6u2zIIjvXHUwMDEw1XdoXHUwMDE1YYnsr+wqXGJ/MGlwyoWKXHUwMDE5XHUwMDEyYUXdo4npqKjBjMGU31x1MDAwNLFcdTAwMTCMU5XIwLJbUVxuhlx1MDAxZtl5YWNkwlx1MDAwMynlYaQ3RVx1MDAxMIIgNDndIYz+ZMeM4CP3LIdcdTAwMTJF51lcYlx1MDAwMVwiXGbr73lcdTAwMTJsQlx1MDAxNjiQN0madYJcdTAwMWSpNqtfpaG5pmGOXHUwMDBmr1x1MDAwM2/WOp1qqGxrqH9lvrb5XHUwMDFhqaO72+zaVm8ryav6L1x1MDAxZFx1MDAwMPt61GHG0vf4519cdTAwMWb++lx1MDAxZow/wb0ifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")match these*don't* match this

    Let's say we want to make the text of the buttons in the dialog bold, but we don't want to change the Button in the sidebar. We can do this with the following rule:

    #dialog Button {\n  text-style: bold;\n}\n

    The #dialog Button selector matches all buttons that are below the widget with an ID of \"dialog\". No other buttons will be matched.

    As with all selectors, you can combine as many as you wish. The following will match a Button that is under a Horizontal widget and under a widget with an id of \"dialog\":

    #dialog Horizontal Button {\n  text-style: bold;\n}\n
    "},{"location":"guide/CSS/#child-combinator","title":"Child combinator","text":"

    The child combinator is similar to the descendant combinator but will only match an immediate child. To create a child combinator, separate two selectors with a greater than symbol (>). Any whitespace around the > will be ignored.

    Let's use this to match the Button in the sidebar given the following DOM:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPayFx1MDAxNv2eX+HyfMmrXHUwMDFhNL0vUzX1ylx1MDAxYl6It1x1MDAxOMdxXqamXHUwMDA0yFi2QFjIYDw1//3dxlx1MDAxOFx0kFx1MDAwNDiIQCaaqlx1MDAxOCTRurp9l3P6dvf8/W5jYzPstZzN3zc2naeq7bm1wO5u/mrOd5yg7fpNuET639v+Y1Dt33lcdTAwMWKGrfbvv/3WsIN7J2x5dtWxOm770fba4WPN9a2q3/jNXHKdRvu/5t9cdTAwMTO74fzR8lx1MDAxYrUwsKKHXHUwMDE0nJpcdTAwMWL6wcuzXHUwMDFjz2k4zbBccq3/XHUwMDBmvm9s/N3/NyZd4FRDu1n3nP5cdTAwMGb6l2JcdTAwMDJcdTAwMTLKx0+f+M2+tJgxxKhiWFx1MDAwZe9w27vwwNCpweVcdTAwMWJcdTAwMTDaia6YU5unT1x1MDAwZr3CcfehXFyq1iv+x6B81Ph4XHUwMDE1PffG9byLsOe96MKu3j5cdTAwMDYxqdph4N87V24tvDVPXHUwMDFmOz/8XdtcdTAwMDc1RL9cbvzH+m3TabdHfuO37KpcdTAwMWL24Fx1MDAxY0fDky9a+H0jOvNkblDIYkQxoZSgSpPhRfNrirhFJOiAIcSU0HxMqlx1MDAxZN+Dnlx1MDAwMKl+Qf0jkqtiV+/rIFxcsza8J1xm7Ga7ZVx1MDAwN9Bf0X3d1/dcdTAwMTVieO7Wceu3IZxUKnqe09e6VlJcdTAwMTKCWNQn5iGtw1rfXHUwMDAy/lx1MDAxY1farVx1MDAxZLRcdTAwMDbK2WybLzFcdTAwMDGNbHvj5lx1MDAxMzehWMdcdTAwMWU3gkcquttb+1x1MDAwNz1153/aqYf+zrCtXHUwMDExe1x1MDAwYp2ncHN44Z9fs5ptfLkslfdvtjvh5aej7nn3+bjeqiQ3a1x1MDAwN4HfnbXdq6NPne5cdTAwMDe2e/wh4LX9XHUwMDFleSRsVyygXXJVOzwo3leP1Vx1MDAxNsPlhne61/xSX0C7Oal3vZr98Lh7gPTnw+enXHUwMDFiVSpcdTAwMTZrT3/Zne5P5S6m2eYneVx1MDAxM1xcf9zzd0J9dl1raX5Fb1ZXucGjLu70xNlflza9Pqtf1vjV0dk3iTtcIsWvs75I1OzgU1x1MDAxNGFcdTAwMWZbNfslXHUwMDEzQuiG5KBcdTAwMDX8p/Twuuc27+Fi89HzonN+9T5Knu9i8k6k7Vx1MDAxMTlHMjZT46eHXHUwMDE5XHUwMDFicoMkSNCZXHUwMDEzdnb3rWrC5lx1MDAxOVx0myBLLidh84SEzaO8PEjYkKtcdTAwMDVcdTAwMTOYxSxjYVx1MDAxOXuRxlx1MDAxOPW531xmL9znvj2JkbNFu+F6vZFu61spSLpcdTAwMDOXbbfpXHUwMDA07782zVx1MDAwNbf2x9fNmmt7fv3r5tfmf+JqbjsgjWmek5F2tjy3bix803NuRk0/dFx1MDAwMftcdTAwMGUvN9xaLY5mq6/PPpxcdTAwMDWD+oFbd5u2V55V8kwvzVx1MDAwNtec41RX5eDHWjOGZ/bVx+1tzfZ6qHhyt9XcVzLsnlx1MDAwNEer7qtCWkQhRDGd9FUmqUVcdTAwMTlTXHUwMDA0XHUwMDEx3vfVXHUwMDFjnVWjSWdVYtxZmWZcdTAwMTBBOc7BV7Oy3e5D8S44Oz54OC/uljpN++7y41x1MDAxN/cnul5cdTAwMTS6zkm969VsXuh6vbRQXHUwMDBlbne7lY5TXHUwMDBl6ZGHiXq+P299+NdpIS8ycNCz5e1e62z3ktxXXHUwMDFmcIufXHUwMDFmXHUwMDFl11x1MDAxNtBuVVx1MDAxY5/xc9ooXHUwMDFl1IJS4aSwfVK8JavYa9NIRvJcdTAwMDOjZlx1MDAwN5++P8ngUoyfXHUwMDFlXCJcdTAwMTdcIjHVUszBMrL1vKLIRZJcZuSipKWWhFxcVFx1MDAwMnKZpFx1MDAxOVpSjaj6sVnGXHUwMDAxgPdng9e99+b8XHUwMDBiYK96drvttFx1MDAwMbVXXHUwMDFlw9BvtpdNOKbg8nHCMc9LZDpvNvdQKHVgXzGwXHUwMDEzhYme2YErXb3fKNRDXSn0XHUwMDBlnXapt394WllxXHUwMDA3ZmZcXJ9LKbg0fkFGPVhcYmJhLFx1MDAxOcKcI+M835t6SI6QknHGuFx1MDAxNOqx5fTY/vmhf1cv71x1MDAxNLYudz63y92Ugbaf1GP+dnNS73o1m1x1MDAxN/VYLy3kRT3WS1x1MDAwYp7e3lGV/Vx1MDAxZLYjhHNRLj3Vi8VcdTAwMTW2hbyYx8LFncY8klx1MDAxZlx1MDAxODU7+PT9mYfSLJV5IFwiIFczOfuYabaeV1x1MDAxNbjwLOCiuSWWXHUwMDAzXFySmEdsaPSVeSBcZlxmXGKxXHUwMDFjgMtcdTAwMTRrjFx1MDAwMau8mcd2XHUwMDFmlb//unntXHUwMDAwNE9mXHUwMDE3mI38bEhcdTAwMWaq8DpO8HZ+MVx1MDAwNXyP84txUTPdMJtDQNem+lwiVlxuXHUwMDBijsXsJOLx+aDTPH1+vmS03PN4VTS/PFx1MDAxN1fcXHUwMDE3wcgspFx1MDAxNXDrXHUwMDE3X1x1MDAxY+NcdTAwMTDYklpcIqXRanBcYkqxXHUwMDE2XFzFRiOWwiGuXHUwMDBlXHUwMDBmzz5cXD8/bFx1MDAxZlx1MDAxNC78QtdcdTAwMGWfPznHPznEojhETupdr2bz4lx1MDAxMOulhbw4xHppIS9cdTAwMGWxXlrIq9iycHGnUZPkXHUwMDA3Rs1cdTAwMGU+LVx1MDAxMlxmZmKiNGpCXHUwMDExkuOnI2pcIjlSfJ6iSLaeV1x1MDAxM1x1MDAwZUnEMuBcdTAwMTAwXHUwMDEzslx1MDAxYzg0XHUwMDFiM8GaKVx1MDAwMmLKXHUwMDFjJkuvIDU58ZfNTKYg+lRm8lwiaaZcdTAwMTO+hKxcdTAwMDQvlFx1MDAwNKU6IcdSKCb57E6YXVx0X00nXHUwMDA0hG9cdTAwMTFwQLAyjlx1MDAxMJd8xFx1MDAwYlx1MDAxOcKWkERx+MC0XCKxQu6i3Vx1MDAxMFlAOFx1MDAxMMZcXEumheI0NnQzdEuhISgwXCKgZ1x1MDAwNHiimvRSjLjCwHDo/F7aXHUwMDE3dtle2lx1MDAwZe0g3HabNbdZh4tRrntdjDPLPMS+X1dcdTAwMWbbfTVcIlx1MDAwMb1IJSeIKOgyLWN31e2W4XpcdTAwMTYolzGEjL4pkXhwwzDnbjrN2nSZskuVMZlcbiBcdTAwMTSViMBcdTAwMTNcdMdA9LlSfEIqYlEphVx1MDAwNsJJNIJAKyak8ux2uOM3XHUwMDFhblxiyj/z3WY4ruS+NreMt986dm38KrxV/Np4WGiZXHUwMDE2R/lp9Gkj8pv+l+HnP39NvjvdnM0xYchRe+/if+eOaJjg1MlcdTAwMTbgXHUwMDE1nDGsZ1x1MDAxZvHMhoUrXHUwMDFh0SS2OIGghSRYN4uiSP/XXHUwMDEyWVx1MDAxYZCVMkNcdTAwMWKaxIZDXHUwMDE3XHUwMDBlKzC1qFCKQWDlbGTSRzTogiymXHUwMDAwXHUwMDAyMZCTgj3EJn5cZuq4mijMwWt+rGg2c+RcdTAwMDD9UI6FMj5EXHUwMDA0pFx1MDAxZlx1MDAxNvOiQdzAkKKwXHUwMDE5NWaIIU5cdTAwMDR7YzTLhFx1MDAxZqMyXHUwMDExqjX4rEJIKFxuXHUwMDFkPSlcdTAwMTO4P9dcZnPCzVxuO1x1MDAxMHytg1m6LZtjwopcdTAwMTdcdTAwMTbLIGWkwjOALIRcbj7HvJPs8tuKXHUwMDA2M8aBIyHMTWKkMjZA/lx1MDAxMs2IxbAwI7VcXFx1MDAwYinHxVpcXDTTXHUwMDFh+phA0NRcdTAwMWNDOoutXHUwMDFhioJcdTAwMTmz4CqVcFx1MDAxZsZE0IlZZVx1MDAxOEJcdTAwMTlcdTAwMDNcdTAwMGbG/9ZoVjCggHFMXHUwMDAwalx1MDAxM1xiaYyihHBGLUhcXFx1MDAwMJOAXHUwMDFlQ1xu44K+LZ5lXHUwMDE3ekal4lx1MDAxYfA/1lx1MDAxMlx1MDAxM4hqmuBcdKG4XHUwMDA1uFx1MDAxYVwirJTIyDUp0jpFs0KqMZtjwoznjGaZRTBcdTAwMTnjJWNcdTAwMDGNaCyQmme1nfZKl6Wb+6fS9dVFrfaJ2Fx1MDAxN1x1MDAxZj6ueDhjZlx1MDAwNY8wuYJAzkYyXG4j/eFcYlx1MDAwMbpcdTAwMDeEKlx1MDAwMbVpXHUwMDA0d+RcdTAwMDfOYlWtKICBbPBoKidgXHUwMDE4QGZcdLQknmyWUlx1MDAwYrvrlu9cXO+ycdTuak4qXi0sep++fbD3+Pzw+br49DlcZnadwu1F+7MkbFx1MDAxMTP2161cdTAwMTaWk3pzalZ23IPiw1x1MDAxZW5cdTAwMDR7f9V2w1x1MDAwZemceovQXHUwMDAyQ0FcdTAwMGI3O9WTq2LpRF3tXvnHYWl1tVu5a2ydeVx1MDAxZIF5WNiu6XrpxH/qra64i197PlDD58NcdTAwMTLx2lx1MDAxN6p1etxmndZt6ZBfflO70yoryVxuipp9zY5cdTAwMGJEYpmJNq2yXHUwMDAySDiVNCDgYkQxOnuWzTaLXHUwMDE1zbJmtUl6ltXCgoSGXHUwMDAx5+mcsyxLyLIk0v1rdqVUamqw1uKza96VlVj9YEpl5aJcdTAwMWE4TvN9Sk1Fjty/sJrKXHUwMDE0jDheU1x1MDAxOcqY6XjpfF2lbyZBgD4gTdjs2z9lR87V9DzOsVx1MDAwNWRcdTAwMWNcXItcdTAwMTGqXGJcdTAwMWatpsB3S0klluB5XHUwMDE4XHUwMDAzi1SUaOCZSopYrXnoiIybMlx1MDAwMFx1MDAwN8ebwLtcdTAwMDQhRpjW4lxyeHeVmXq2O2zEx/i4llx1MDAwMuJcdTAwMTJcdTAwMDeWLijhMYo4oMTMQlx1MDAxY1RcZtpcdTAwMWJcXJmTn89R0Vx1MDAxMVx1MDAxOFx0XHUwMDA19JxhXHSBMqGgXHUwMDAzsiCgsohzbpKb1lx1MDAxM1wirVx1MDAxMz9PN15zxMw2auhd/O/bpqfSdGpcdTAwMGVsUYE7izm217huPV2497RyfcnK4fn+XHUwMDE26dyffl7x4EWJtKggXHUwMDFjSHpcdTAwMDJsMJvXYWVqwfqFnH/33eugQ1x1MDAxOFn+XCK34z1WPFdq9+amVun2yrf+VlhOISA/J6jO325O6l2vZnPbvW6ttJBTs7ntXpePuHmNIOQk7nn9oX7uXFw9XHUwMDEwLb94lyeNtt1yUsZRXHUwMDE2tdle4otEzb6Cg+89MEEoSV1cdTAwMDFDMCFCKI5nh1x1MDAxONn9t6JcdTAwMTCDkiyIQVx1MDAwMOsuXHRizLbfXHUwMDFlxpIjSlx1MDAwMX2v39DEN+6313ZrTsVcdTAwMGWWvf/FXHUwMDE04DzThnsjome66pRcdTAwMTVr6ZtjMsaAiDE++3DG9lHTx1xi6JdDjypP5Vx1MDAxMr7URSfFXcfcbtRZx6csvdVZMZrqrVhbXGZpRvCLt45OPmCCWIJcbo6Y+PbRjF8wqSgllEpwVZkweJGw5Vx1MDAwNSZcdTAwMTKx+Fx1MDAwNNalsIHzvaND9CRLN6dXLr66PdNfetf7P9nAothATupdr2bzYlx1MDAwM+ulhbyWq62XXHUwMDE28lqulpO4eW15sV6dtnxOlPxcIjOKe1rd3T65bqn60Ze7Y7vje0/bjM0m7uDTd+daXHUwMDE00dTldUT3t9mLIYVp2C3bLL5cdTAwMGLVmlx1MDAwMb1cdTAwMTGWhd6ktPCi0NuU0dxcdTAwMDT8lrC1uWRMXHUwMDAzXHUwMDA0zFx1MDAwMb+tXHUwMDBl01x1MDAxYSxaM+de+MrXzcNmO7Q9b9k8a1xuXHUwMDFkSVltlyp4ppOmXHUwMDE3jFx1MDAxOUl1Ulx1MDAwNCleUDVcdTAwMDfDyp5cdTAwMTKzmlx1MDAwM1winHFLMCp14oBcYuHCMsMgS3BSxixJiOCCJMzZYNriQjAkJkvFVCBwJPKWXUJ+iEpxwVxmWWlcIoSmXHUwMDEwZzWmOKk8yyjlmsoppeJR+deoYltIMlx1MDAxZnPEXGYnauNd/O/8MYOn1mk55cpUjGfP65m4bDUjXHUwMDA2w8Si4OuvQ6hcdCt2XHUwMDExNsaY91x1MDAxY1x1MDAxM2RcdTAwMTFuZvQogYmAXGJcdTAwMTCb2Fx1MDAxNa3XNZNcdTAwMTckVlx0U6qppGZcdTAwMDe2t+T7VVx1MDAwZVx1MDAxY9njl6OBXHUwMDAzXHUwMDExJFx1MDAxMWdUSCRcdTAwMTiOZlx1MDAxN1xmXHUwMDAzh7RcdTAwMTQliGIzXHUwMDE3XHUwMDA3joR1XHUwMDE3M001yc71XHUwMDFiI4uHQVx1MDAxY7PGiygzUYlINLlOd3K18DqFqnSrNUdkr3OGq3RcdTAwMWWSselcdTAwMTnHRFx1MDAxMqlm5yFS2yd35O7sYLdXrX/0xd72WUWkxKtcdTAwMTVcdTAwMTlDJlx1MDAxNFx1MDAwMI7Go9jhZb8z9mrd31TmyVx1MDAxODkmSdPeJiFcZlx1MDAwNtJcdTAwMDH2IN8y2+1bho5Xi2YnXHUwMDExm9lnt142a05gXGLLRnjrtjde9lx1MDAxZE+e6qpGfjz7VNfQb6WxmZHXXHUwMDFhpy7Jor1ccnxA0EhcdTAwMDVcdTAwMWbIJGQ5x+L67P5fSW+mWFlcdTAwMTC2zNbiXHUwMDE0Y6FGx1x1MDAxNFx1MDAwNMA9zNToXG6qMY92hJRv92hNLU0pXHUwMDE1ZjNcdTAwMWUmaGxCXnxcdTAwMTVX4lx1MDAxYS6zmo/QkZLdXHUwMDBmXHUwMDAxOLJzwijggNxcdTAwMGXAXHUwMDExXHUwMDExXGKMWkNATli4XHUwMDBl0VrNwlW+XHUwMDE5a1x1MDAxOHGkJmbSqlZcdTAwMDSeiVTiYliwMqzM/zvDbFx1MDAwMrXe2CPVes1ReDXcNOTxbtDupt1qXYRgZcNeXHUwMDAwQ3Zrg5BcdTAwMWS93GbHdbrbXHTuddM/TFx1MDAwNOyr0Vx1MDAwNFx1MDAxYce84t//vPvn/yxzRNUifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")Underline this button

    We can use the following CSS to style all buttons which have a parent with an ID of sidebar:

    #sidebar > Button {\n  text-style: underline;\n}\n
    "},{"location":"guide/CSS/#specificity","title":"Specificity","text":"

    It is possible that several selectors match a given widget. If the same style is applied by more than one selector then Textual needs a way to decide which rule wins. It does this by following these rules:

    • The selector with the most IDs wins. For instance #next beats .button and #dialog #next beats #next. If the selectors have the same number of IDs then move to the next rule.

    • The selector with the most class names wins. For instance .button.success beats .success. For the purposes of specificity, pseudo classes are treated the same as regular class names, so .button:hover counts as 2 class names. If the selectors have the same number of class names then move to the next rule.

    • The selector with the most types wins. For instance Container Button beats Button.

    "},{"location":"guide/CSS/#important-rules","title":"Important rules","text":"

    The specificity rules are usually enough to fix any conflicts in your stylesheets. There is one last way of resolving conflicting selectors which applies to individual rules. If you add the text !important to the end of a rule then it will \"win\" regardless of the specificity.

    Warning

    Use !important sparingly (if at all) as it can make it difficult to modify your CSS in the future.

    Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:

    Button:hover {\n  background: blue !important;\n}\n
    "},{"location":"guide/CSS/#css-variables","title":"CSS Variables","text":"

    You can define variables to reduce repetition and encourage consistency in your CSS. Variables in Textual CSS are prefixed with $. Here's an example of how you might define a variable called $border:

    $border: wide green;\n

    With our variable assigned, we can write $border and it will be substituted with wide green. Consider the following snippet:

    #foo {\n  border: $border;\n}\n

    This will be translated into:

    #foo {\n  border: wide green;\n}\n

    Variables allow us to define reusable styling in a single place. If we decide we want to change some aspect of our design in the future, we only have to update a single variable.

    Note

    Variables can only be used in the values of a CSS declaration. You cannot, for example, refer to a variable inside a selector.

    Variables can refer to other variables. Let's say we define a variable $success: lime;. Our $border variable could then be updated to $border: wide $success;, which will be translated to $border: wide lime;.

    "},{"location":"guide/CSS/#initial-value","title":"Initial value","text":"

    All CSS rules support a special value called initial, which will reset a value back to its default.

    Let's look at an example. The following will set the background of a button to green:

    Button {\n  background: green;\n}\n

    If we want a specific button (or buttons) to use the default color, we can set the value to initial. For instance, if we have a widget with a (CSS) class called dialog, we could reset the background color of all buttons inside the dialog with the following CSS:

    .dialog Button {\n  background: initial;\n}\n

    Note that initial will set the value back to the value defined in any default css. If you use initial within default css, it will treat the rule as completely unstyled.

    "},{"location":"guide/CSS/#nesting-css","title":"Nesting CSS","text":"

    Added in version 0.47.0

    CSS rule sets may be nested, i.e. they can contain other rule sets. When a rule set occurs within an existing rule set, it inherits the selector from the enclosing rule set.

    Let's put this into practical terms. The following example will display two boxes containing the text \"Yes\" and \"No\" respectively. These could eventually form the basis for buttons, but for this demonstration we are only interested in the CSS.

    nesting01.tcss (no nesting)nesting01.pyOutput
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n}\n\n/* Style all buttons */\n#questions .button {\n    width: 1fr;\n    padding: 1 2;\n    margin: 1 2;\n    text-align: center;\n    border: heavy $panel;\n}\n\n/* Style the Yes button */\n#questions .button.affirmative {\n    border: heavy $success;\n}\n\n/* Style the No button */\n#questions .button.negative {\n    border: heavy $error;\n}\n
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static\n\n\nclass NestingDemo(App):\n    \"\"\"App that doesn't have nested CSS.\"\"\"\n\n    CSS_PATH = \"nesting01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"questions\"):\n            yield Static(\"Yes\", classes=\"button affirmative\")\n            yield Static(\"No\", classes=\"button negative\")\n\n\nif __name__ == \"__main__\":\n    app = NestingDemo()\n    app.run()\n

    NestingDemo \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Yes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0No\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    The CSS is quite straightforward; there is one rule for the container, one for all buttons, and one rule for each of the buttons. However it is easy to imagine this stylesheet growing more rules as we add features.

    Nesting allows us to group rule sets which have common selectors. In the example above, the rules all start with #questions. When we see a common prefix on the selectors, this is a good indication that we can use nesting.

    The following produces identical results to the previous example, but adds nesting of the rules.

    nesting02.tcss (with nesting)nesting02.pyOutput
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n\n    /* Style all buttons */\n    .button {\n        width: 1fr;\n        padding: 1 2;\n        margin: 1 2;\n        text-align: center;\n        border: heavy $panel;    \n\n        /* Style the Yes button */\n        &.affirmative {\n            border: heavy $success;        \n        }\n\n        /* Style the No button */\n        &.negative {\n            border: heavy $error;\n        }\n    }\n}\n
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static\n\n\nclass NestingDemo(App):\n    \"\"\"App with nested CSS.\"\"\"\n\n    CSS_PATH = \"nesting02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"questions\"):\n            yield Static(\"Yes\", classes=\"button affirmative\")\n            yield Static(\"No\", classes=\"button negative\")\n\n\nif __name__ == \"__main__\":\n    app = NestingDemo()\n    app.run()\n

    NestingDemo \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Yes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0No\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Tip

    Indenting the rule sets is not strictly required, but it does make it easier to understand how the rule sets are related to each other.

    In the first example we had a rule set that began with the selector #questions .button, which would match any widget with a class called \"button\" that is inside a container with id questions.

    In the second example, the button rule selector is simply .button, but it is within the rule set with selector #questions. The nesting means that the button rule set will inherit the selector from the outer rule set, so it is equivalent to #questions .button.

    "},{"location":"guide/CSS/#nesting-selector","title":"Nesting selector","text":"

    The two remaining rules are nested within the button rule, which means they will inherit their selectors from the button rule set and the outer #questions rule set.

    You may have noticed that the rules for the button styles contain a syntax we haven't seen before. The rule for the Yes button is &.affirmative. The ampersand (&) is known as the nesting selector and it tells Textual that the selector should be combined with the selector from the outer rule set.

    So &.affirmative in the example above, produces the equivalent of #questions .button.affirmative which selects a widget with both the button and affirmative classes. Without & it would be equivalent to #questions .button .affirmative (note the additional space) which would only match a widget with class affirmative inside a container with class button.

    For reference, lets see those two CSS files side-by-side:

    nesting01.tcssnesting02.tcss
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n}\n\n/* Style all buttons */\n#questions .button {\n    width: 1fr;\n    padding: 1 2;\n    margin: 1 2;\n    text-align: center;\n    border: heavy $panel;\n}\n\n/* Style the Yes button */\n#questions .button.affirmative {\n    border: heavy $success;\n}\n\n/* Style the No button */\n#questions .button.negative {\n    border: heavy $error;\n}\n
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n\n    /* Style all buttons */\n    .button {\n        width: 1fr;\n        padding: 1 2;\n        margin: 1 2;\n        text-align: center;\n        border: heavy $panel;    \n\n        /* Style the Yes button */\n        &.affirmative {\n            border: heavy $success;        \n        }\n\n        /* Style the No button */\n        &.negative {\n            border: heavy $error;\n        }\n    }\n}\n

    Note how nesting bundles related rules together. If we were to add other selectors for additional screens or widgets, it would be easier to find the rules which will be applied.

    "},{"location":"guide/CSS/#why-use-nesting","title":"Why use nesting?","text":"

    There is no requirement to use nested CSS, but it can help to group related rule sets together (which makes it easier to edit). Nested CSS can also help you avoid some repetition in your selectors, i.e. in the nested CSS we only need to type #questions once, rather than four times in the non-nested CSS.

    "},{"location":"guide/actions/","title":"Actions","text":"

    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.

    "},{"location":"guide/actions/#action-methods","title":"Action methods","text":"

    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\nfrom textual import events\n\n\nclass ActionsApp(App):\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n    def on_key(self, event: events.Key) -> None:\n        if event.key == \"r\":\n            self.action_set_background(\"red\")\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    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\nfrom textual.app import App\n\n\nclass ActionsApp(App):\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n    async def on_key(self, event: events.Key) -> None:\n        if event.key == \"r\":\n            await self.run_action(\"set_background('red')\")\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    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.

    "},{"location":"guide/actions/#syntax","title":"Syntax","text":"

    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')"},{"location":"guide/actions/#parameters","title":"Parameters","text":"

    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 \u2014 because new_color is a variable and not a literal.

    "},{"location":"guide/actions/#links","title":"Links","text":"

    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.pyOutput actions03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\n\n\nclass ActionsApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    ActionsApp Set\u00a0your\u00a0background Red Green Blue

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

    "},{"location":"guide/actions/#bindings","title":"Bindings","text":"

    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.pyOutput actions04.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\n\n\nclass ActionsApp(App):\n    BINDINGS = [\n        (\"r\", \"set_background('red')\", \"Red\"),\n        (\"g\", \"set_background('green')\", \"Green\"),\n        (\"b\", \"set_background('blue')\", \"Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    ActionsApp Set\u00a0your\u00a0background 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.

    "},{"location":"guide/actions/#namespaces","title":"Namespaces","text":"

    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.pyactions05.tcss actions05.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('cyan')]Cyan[/]\n[@click=set_background('magenta')]Magenta[/]\n[@click=set_background('yellow')]Yellow[/]\n\"\"\"\n\n\nclass ColorSwitcher(Static):\n    def action_set_background(self, color: str) -> None:\n        self.styles.background = color\n\n\nclass ActionsApp(App):\n    CSS_PATH = \"actions05.tcss\"\n    BINDINGS = [\n        (\"r\", \"set_background('red')\", \"Red\"),\n        (\"g\", \"set_background('green')\", \"Green\"),\n        (\"b\", \"set_background('blue')\", \"Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield ColorSwitcher(TEXT)\n        yield ColorSwitcher(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n
    actions05.tcss
    Screen {\n    layout: grid;\n    grid-size: 1;\n    grid-gutter: 2 4;\n    grid-rows: 1fr;\n}\n\nColorSwitcher {\n   height: 100%;\n   margin: 2 4;\n}\n

    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.

    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').

    "},{"location":"guide/actions/#builtin-actions","title":"Builtin actions","text":"

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

    • action_add_class
    • action_back
    • action_bell
    • action_check_bindings
    • action_focus
    • action_focus_next
    • action_focus_previous
    • action_pop_screen
    • action_push_screen
    • action_quit
    • action_remove_class
    • action_screenshot
    • action_switch_screen
    • action_toggle_class
    • action_toggle_dark
    "},{"location":"guide/animation/","title":"Animation","text":"

    Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.

    "},{"location":"guide/animation/#animating-styles","title":"Animating styles","text":"

    Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to styles such as offset to move widgets around the screen, and opacity to create fading effects.

    Apps and widgets both have an animate method which will animate properties on those objects. Additionally, styles objects have an identical animate method which will animate styles.

    Let's look at an example of how we can animate the opacity of a widget to make it fade out. The following example app contains a single Static widget which is immediately animated to an opacity of 0.0 (making it invisible) over a duration of two seconds.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass AnimationApp(App):\n    def compose(self) -> ComposeResult:\n        self.box = Static(\"Hello, World!\")\n        self.box.styles.background = \"red\"\n        self.box.styles.color = \"black\"\n        self.box.styles.padding = (1, 2)\n        yield self.box\n\n    def on_mount(self):\n        self.box.styles.animate(\"opacity\", value=0.0, duration=2.0)\n\n\nif __name__ == \"__main__\":\n    app = AnimationApp()\n    app.run()\n

    The animator updates the value of the opacity attribute on the styles object in small increments over two seconds. Here's what the output will look like after each half a second.

    After 0sAfter 0.5sAfter 1sAfter 1.5sAfter 2s

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    "},{"location":"guide/animation/#duration-and-speed","title":"Duration and Speed","text":"

    When requesting an animation you can specify a duration or speed. The duration is how long the animation should take in seconds. The speed is how many units a value should change in one second. For instance, if you animate a value at 0 to 10 with a speed of 2, it will complete in 5 seconds.

    "},{"location":"guide/animation/#easing-functions","title":"Easing functions","text":"

    The easing function determines the journey a value takes on its way to the target value. It could move at a constant pace, or it might start off slow then accelerate towards its final value. Textual supports a number of easing functions.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1XWVPjRlx1MDAxMH7nV1DeVyzmPrYqlYJcdTAwMDVcdTAwMTJcdTAwMDJLXHUwMDBlczib2ofBXHUwMDFh21x1MDAxM8uSkMZcdTAwMDaW4r+nJVx1MDAxM48vwrnUblX0YGumZ3q+6e6vu3Wztr7e8Ne5bbxfb9irjklcXFxcmMvGRjU/tkXpslx1MDAxNESkXHUwMDFll9mo6NQr+97n5fvNzaEpXHUwMDA21ueJ6dho7MqRSUo/il1cdTAwMTZ1suGm83ZY/lj9XHUwMDFlmaH9Ic+GsS+icEjTxs5nxeQsm9ihTX1cdNr/gvH6+k39O4POXHUwMDE0RTZcdTAwMDFWT1x1MDAwN3BKL05cdTAwMWVlaY1cdTAwMTMjrjSjWuDpXG5X7sBR3sYg7lx1MDAwMlxcXHUwMDFiJNVU42Tvl7TZ/nX76rh1fDDO+4j+dNVcdKd2XZK0/HUysYLp9EeFXHLS0lx1MDAxN9nAnrnY9/812sz8dF9syj5cdTAwMDCYiots1OuntqwuXHUwMDFmkGa56Th/XV9cdTAwMDNNZ03aq5WEmStcdTAwMThRwSMqXHUwMDE0XHUwMDE3XGZRQZhUU2m1n2lcdTAwMWNxXHUwMDAxcsyRRIxcdTAwMTK5gOxDloBcdTAwMWZcdTAwMDDZO1Q/XHUwMDAx2rnpXGZ6gC+Np2t8YdIyN1x1MDAwNXgrrLu8uzOONGZIXHUwMDEwPpX0rev1fWVcdTAwMGXCXCIlg6C0tVx1MDAxM7DCmiuhUZBUXHUwMDA35vtxXHUwMDFkXHUwMDBin1x1MDAxN43YN0V+Z6tGXHJsXHUwMDA2bDXcnVx0pLB5lMdm4nQsXHUwMDA0x1xcXHUwMDEzWtlqKk9cXDpcdTAwMDBhOkqSMJd1XHUwMDA2K+Kk9Kbw2y6NXdpb3GLT+Fx1MDAxZUliSv8hXHUwMDFiXHUwMDBlnVx1MDAwN1x1MDAxOL9lLvWLK2q9W1WQ962JV2ielS2yIa80XHUwMDA28lRPeFtcdTAwMGbRUlx1MDAwZqbvnzdWrm4uebGevXNg2L02+3+78UTa4pkwXFzkraCaIaxkYPZDvKUnh1x1MDAwN7y9d/7n3uFheaJHgra3+t9cdTAwMDFvRUQgXHUwMDE2pdRaXHUwMDAwbdlcdTAwMTJvMaFIYES1XHUwMDE0mi9cdTAwMDB7PdpShFwiKSmjclx1MDAwNXFxJCFtUEyWuEs5Ykpr9sbUhdAgwY7/U3dmwVxuR1ZPM/jwifRcdTAwMDXr2pXsRfL+skuYpkpjXHUwMDFj8utD9G3r38nu2cfty7//6F1cZoa7R1x1MDAwM3aQvDJ9y1xm+o7Xr7pUXHUwMDAwNSWjeJagNX2VjJCQXFxTRojmSC9cdTAwMDBcdTAwMGL0VYJY3XlcdH1pJKSCUsqpRFqH8lx1MDAxZqovXHUwMDAyMFx1MDAwNHMhJWGKXHUwMDEyvURmgpnAXHUwMDAyIf6mbFZcbmCrXHUwMDAw+Htj85zsVamMMYso4lpqSoRcdTAwMTYqNLHV06QogsZcdTAwMTZxJjWD5lx0yVx1MDAwN/VpXHUwMDEyKVwiJHTDoJJzzOb0YYVcIkisWFIou1x1MDAxMlx1MDAwYvVwqrkn6Gp1y/H2xMTj7ZVflXjEfVmnSjpcdTAwMDRcbkPoKlx1MDAxZUo6O+3Wyadd0vz482D3tDX+1L44z/Hzklx1MDAwZV5kx1dLOuDsiDBcdTAwMDZdPuJcdTAwMDRcbvB8q89cdHiRay5cdTAwMTTl0PD/R855acvAXHUwMDAyrJBklrt7wYggirFw2UcnlbJcdTAwMWE8M6lQKlmIlCcklW6W+pb7Ulx1MDAwN1x1MDAxNJqb3TNDl1xcz/mtjtHKUG5oZy1Z2rpsVlx1MDAxZjdza7dcdTAwMTLXS+uqarvzwe1cdTAwMWR8XHUwMDE1T8U+m7l3XHUwMDA3zjagrthfyjdZ4XouNcnxLI5nsYrcW8zhq1x1MDAxMVPIXG6Pp9Vps9y/lGdcdTAwMTe90+ukvfOlO+52bfNbp1x1MDAxNYH0x1x1MDAxNaZcXCqFkObznTiQKVx1MDAxMlxcK2jEXHUwMDE5NFLQkn81XnHyKF5BktaKMvyMYv1cdTAwMTJesSrLvlx1MDAxOa/GJlx1MDAxOX1cdTAwMTPEmlx1MDAwMJkwa+2uXCI2TJ63PNhcdTAwMDdcdTAwMTZMeFx1MDAwNi5w8d0lg7rG2NnL7eUoeNetn0przdaKXHUwMDE4tnLAze3a7T8uXFzYXHUwMDFjIn0= timevalue

    Run the following from the command prompt to preview them.

    textual easing\n

    You can specify which easing method to use via the easing parameter on the animate method. The default easing method is \"in_out_cubic\" which accelerates and then decelerates to produce a pleasing organic motion.

    Note

    The textual easing preview requires the textual-dev package to be installed (using pip install textual-dev).

    "},{"location":"guide/animation/#completion-callbacks","title":"Completion callbacks","text":"

    You can pass a callable to the animator via the on_complete parameter. Textual will run the callable when the animation has completed.

    "},{"location":"guide/animation/#delaying-animations","title":"Delaying animations","text":"

    You can delay the start of an animation with the delay parameter of the animate method. This parameter accepts a float value representing the number of seconds to delay the animation by. For example, self.box.styles.animate(\"opacity\", value=0.0, duration=2.0, delay=5.0) delays the start of the animation by five seconds, meaning the animation will start after 5 seconds and complete 2 seconds after that.

    "},{"location":"guide/app/","title":"App Basics","text":"

    In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters.

    "},{"location":"guide/app/#the-app-class","title":"The App class","text":"

    The first step in building a Textual app is to import the App class and create a subclass. Let's look at the simplest app class:

    from textual.app import App\n\n\nclass MyApp(App):\n    pass\n
    "},{"location":"guide/app/#the-run-method","title":"The run method","text":"

    To run an app we create an instance and call run().

    simple02.py
    from textual.app import App\n\n\nclass MyApp(App):\n    pass\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n

    Apps don't get much simpler than this\u2014don't expect it to do much.

    Tip

    The __name__ == \"__main__\": condition is true only if you run the file with python command. This allows us to import app without running the app immediately. It also allows the devtools run command to run the app in development mode. See the Python docs for more information.

    If we run this app with python simple02.py you will see a blank terminal, something like the following:

    MyApp

    When you call App.run() Textual puts the terminal in to a special state called application mode. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the screen).

    If you hit Ctrl+C Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored.

    Tip

    A side effect of application mode is that you may no longer be able to select and copy text in the usual way. Terminals typically offer a way to bypass this limit with a key modifier. On iTerm you can select text if you hold the Option key. See the documentation for your terminal software for how to select text in application mode.

    "},{"location":"guide/app/#events","title":"Events","text":"

    Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods prefixed with on_ followed by the name of the event.

    One such event is the mount event which is sent to an application after it enters application mode. You can respond to this event by defining a method called on_mount.

    Info

    You may have noticed we use the term \"send\" and \"sent\" in relation to event handler methods in preference to \"calling\". This is because Textual uses a message passing system where events are passed (or sent) between components. See events for details.

    Another such event is the key event which is sent when the user presses a key. The following example contains handlers for both those events:

    event01.py
    from textual.app import App\nfrom textual import events\n\n\nclass EventApp(App):\n\n    COLORS = [\n        \"white\",\n        \"maroon\",\n        \"red\",\n        \"purple\",\n        \"fuchsia\",\n        \"olive\",\n        \"yellow\",\n        \"navy\",\n        \"teal\",\n        \"aqua\",\n    ]\n\n    def on_mount(self) -> None:\n        self.screen.styles.background = \"darkblue\"\n\n    def on_key(self, event: events.Key) -> None:\n        if event.key.isdecimal():\n            self.screen.styles.background = self.COLORS[int(event.key)]\n\n\nif __name__ == \"__main__\":\n    app = EventApp()\n    app.run()\n

    The on_mount handler sets the self.screen.styles.background attribute to \"darkblue\" which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.

    EventApp

    The key event handler (on_key) has an event parameter which will receive a Key instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.

    Note

    It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.

    Some events contain additional information you can inspect in the handler. The Key event has a key attribute which is the name of the key that was pressed. The on_key method above uses this attribute to change the background color if any of the keys from 0 to 9 are pressed.

    "},{"location":"guide/app/#async-events","title":"Async events","text":"

    Textual is powered by Python's asyncio framework which uses the async and await keywords.

    Textual knows to await your event handlers if they are coroutines (i.e. prefixed with the async keyword). Regular functions are generally fine unless you plan on integrating other async libraries (such as httpx for reading data from the internet).

    Tip

    For a friendly introduction to async programming in Python, see FastAPI's concurrent burgers article.

    "},{"location":"guide/app/#widgets","title":"Widgets","text":"

    Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.

    Widgets can be as simple as a piece of text, a button, or a fully-fledged component like a text editor or file browser (which may contain widgets of their own).

    "},{"location":"guide/app/#composing","title":"Composing","text":"

    To add widgets to your app implement a compose() method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a generator.

    The following example imports a builtin Welcome widget and yields it from App.compose().

    widgets01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Welcome\n\n\nclass WelcomeApp(App):\n    def compose(self) -> ComposeResult:\n        yield Welcome()\n\n    def on_button_pressed(self) -> None:\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    When you run this code, Textual will mount the Welcome widget which contains Markdown content and a button:

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Notice the on_button_pressed method which handles the Button.Pressed event sent by a button contained in the Welcome widget. The handler calls App.exit() to exit the app.

    "},{"location":"guide/app/#mounting","title":"Mounting","text":"

    While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling mount() which will add a new widget to the UI.

    Here's an app which adds a welcome widget in response to any key press:

    widgets02.py
    from textual.app import App\nfrom textual.widgets import Welcome\n\n\nclass WelcomeApp(App):\n    def on_key(self) -> None:\n        self.mount(Welcome())\n\n    def on_button_pressed(self) -> None:\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503\u2582\u2582 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0 \u258c\u00a0that\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0 \u258c\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn \u258c\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 \u258c\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#awaiting-mount","title":"Awaiting mount","text":"

    When you mount a widget, Textual will mount everything the widget composes. Textual guarantees that the mounting will be complete by the next message handler, but not immediately after the call to mount(). This may be a problem if you want to make any changes to the widget in the same message handler.

    Let's first illustrate the problem with an example. The following code will mount the Welcome widget in response to a key press. It will also attempt to modify the Button in the Welcome widget by changing its label from \"OK\" to \"YES!\".

    from textual.app import App\nfrom textual.widgets import Button, Welcome\n\n\nclass WelcomeApp(App):\n    def on_key(self) -> None:\n        self.mount(Welcome())\n        self.query_one(Button).label = \"YES!\" # (1)!\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n
    1. See queries for more information on the query_one method.

    If you run this example, you will find that Textual raises a NoMatches exception when you press a key. This is because the mount process has not yet completed when we attempt to change the button.

    To solve this we can optionally await the result of mount(), which requires we make the function async. This guarantees that by the following line, the Button has been mounted, and we can change its label.

    from textual.app import App\nfrom textual.widgets import Button, Welcome\n\n\nclass WelcomeApp(App):\n    async def on_key(self) -> None:\n        await self.mount(Welcome())\n        self.query_one(Button).label = \"YES!\"\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    Here's the output. Note the changed button text:

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YES! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#exiting","title":"Exiting","text":"

    An app will run until you call App.exit() which will exit application mode and the run method will return. If this is the last line in your code you will return to the command prompt.

    The exit method will also accept an optional positional value to be returned by run(). The following example uses this to return the id (identifier) of a clicked button.

    question01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    Running this app will give you the following:

    QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Yes \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 No \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Clicking either of those buttons will exit the app, and the run() method will return either \"yes\" or \"no\" depending on button clicked.

    "},{"location":"guide/app/#return-type","title":"Return type","text":"

    You may have noticed that we subclassed App[str] rather than the usual App.

    question01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    The addition of [str] tells mypy that run() is expected to return a string. It may also return None if App.exit() is called without a return value, so the return type of run will be str | None. Replace the str in [str] with the type of the value you intend to call the exit method with.

    Note

    Type annotations are entirely optional (but recommended) with Textual.

    "},{"location":"guide/app/#return-code","title":"Return code","text":"

    When you exit a Textual app with App.exit(), you can optionally specify a return code with the return_code parameter.

    What are return codes?

    Returns codes are a standard feature provided by your operating system. When any application exits it can return an integer to indicate if it was successful or not. A return code of 0 indicates success, any other value indicates that an error occurred. The exact meaning of a non-zero return code is application-dependant.

    When a Textual app exits normally, the return code will be 0. If there is an unhandled exception, Textual will set a return code of 1. You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception.

    Here's an example of setting a return code for an error condition:

    if critical_error:\n    self.exit(return_code=4, message=\"Critical error occurred\")\n

    The app's return code can be queried with app.return_code, which will be None if it hasn't been set, or an integer.

    Textual won't explicitly exit the process. To exit the app with a return code, you should call sys.exit. Here's how you might do that:

    if __name__ == \"__main__\"\n    app = MyApp()\n    app.run()\n    import sys\n    sys.exit(app.return_code or 0)\n
    "},{"location":"guide/app/#css","title":"CSS","text":"

    Textual apps can reference CSS files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).

    Info

    Textual apps typically use the extension .tcss for external CSS files to differentiate them from browser (.css) files.

    The chapter on Textual CSS describes how to use CSS in detail. For now let's look at how your app references external CSS files.

    The following example enables loading of CSS by adding a CSS_PATH class variable:

    question02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Label\n\n\nclass QuestionApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    Note

    We also added an id to the Label, because we want to style it in the CSS.

    If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references \"question01.tcss\" in the same directory as the Python code. Here is that CSS file:

    question02.tcss
    Screen {\n    layout: grid;\n    grid-size: 2;\n    grid-gutter: 2;\n    padding: 2;\n}\n#question {\n    width: 100%;\n    height: 100%;\n    column-span: 2;\n    content-align: center bottom;\n    text-style: bold;\n}\n\nButton {\n    width: 100%;\n}\n

    When \"question02.py\" runs it will load \"question02.tcss\" and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:

    QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#classvar-css","title":"Classvar CSS","text":"

    While external CSS files are recommended for most applications, and enable some cool features like live editing, you can also specify the CSS directly within the Python code.

    To do this set a CSS class variable on the app to a string containing your CSS.

    Here's the question app with classvar CSS:

    question03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    CSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n        grid-gutter: 2;\n        padding: 2;\n    }\n    #question {\n        width: 100%;\n        height: 100%;\n        column-span: 2;\n        content-align: center bottom;\n        text-style: bold;\n    }\n\n    Button {\n        width: 100%;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n
    "},{"location":"guide/app/#title-and-subtitle","title":"Title and subtitle","text":"

    Textual apps have a title attribute which is typically the name of your application, and an optional sub_title attribute which adds additional context (such as the file your are working on). By default, title will be set to the name of your App class, and sub_title is empty. You can change these defaults by defining TITLE and SUB_TITLE class variables. Here's an example of that:

    question_title01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Header, Label\n\n\nclass MyApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n    TITLE = \"A Question App\"\n    SUB_TITLE = \"The most important question\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    reply = app.run()\n    print(reply)\n

    Note that the title and subtitle are displayed by the builtin Header widget at the top of the screen:

    A\u00a0Question\u00a0App \u2b58A\u00a0Question\u00a0App\u00a0\u2014\u00a0The\u00a0most\u00a0important\u00a0question Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    You can also set the title attributes dynamically within a method of your app. The following example sets the title and subtitle in response to a key press:

    question_title02.py
    from textual.app import App, ComposeResult\nfrom textual.events import Key\nfrom textual.widgets import Button, Header, Label\n\n\nclass MyApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n    TITLE = \"A Question App\"\n    SUB_TITLE = \"The most important question\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n    def on_key(self, event: Key):\n        self.title = event.key\n        self.sub_title = f\"You just pressed {event.key}!\"\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    reply = app.run()\n    print(reply)\n

    If you run this app and press the T key, you should see the header update accordingly:

    A\u00a0Question\u00a0App \u2b58t\u00a0\u2014\u00a0You\u00a0just\u00a0pressed\u00a0t! Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Info

    Note that there is no need to explicitly refresh the screen when setting the title attributes. This is an example of reactivity, which we will cover later in the guide.

    "},{"location":"guide/app/#whats-next","title":"What's next","text":"

    In the following chapter we will learn more about how to apply styles to your widgets and app.

    "},{"location":"guide/command_palette/","title":"Command Palette","text":"

    Textual apps have a built-in command palette, which gives users a quick way to access certain functionality within your app.

    In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands.

    "},{"location":"guide/command_palette/#launching-the-command-palette","title":"Launching the command palette","text":"

    Press Ctrl + \\ (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget. Textual will suggest commands as you type in that input. Press Up or Down to select a command from the list, and Enter to invoke it.

    Commands are looked up via a fuzzy search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command. For instance the \"Toggle light/dark mode\" command will be shown if you type \"to\" (for toggle), but you could also type \"dm\" (to match dark mode). This scheme allows the user to quickly get to a particular command with a minimum of key-presses.

    Command PaletteCommand Palette after 't'Command Palette after 'td'

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0eCommand\u00a0Palette\u00a0Search... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0et Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Ring\u00a0the\u00a0bell Ring\u00a0the\u00a0terminal's\u00a0'bell' \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0etd Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/command_palette/#default-commands","title":"Default commands","text":"

    Textual apps have the following commands enabled by default:

    • \"Toggle light/dark mode\" This will toggle between light and dark mode, by setting App.dark to either True or False.
    • \"Quit the application\" Quits the application. The equivalent of pressing ++ctrl+C++.
    • \"Play the bell\" Plays the terminal bell, by calling App.bell.
    "},{"location":"guide/command_palette/#command-providers","title":"Command providers","text":"

    To add your own command(s) to the command palette, define a command.Provider class then add it to the COMMANDS class var on your App class.

    Let's look at a simple example which adds the ability to open Python files via the command palette.

    The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it.

    Tip

    If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files.

    command01.py
    from __future__ import annotations\n\nfrom functools import partial\nfrom pathlib import Path\n\nfrom textual.app import App, ComposeResult\nfrom textual.command import Hit, Hits, Provider\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Static\n\n\nclass PythonFileCommands(Provider):\n    \"\"\"A command provider to open a Python file in the current working directory.\"\"\"\n\n    def read_files(self) -> list[Path]:\n        \"\"\"Get a list of Python files in the current working directory.\"\"\"\n        return list(Path(\"./\").glob(\"*.py\"))\n\n    async def startup(self) -> None:  # (1)!\n        \"\"\"Called once when the command palette is opened, prior to searching.\"\"\"\n        worker = self.app.run_worker(self.read_files, thread=True)\n        self.python_paths = await worker.wait()\n\n    async def search(self, query: str) -> Hits:  # (2)!\n        \"\"\"Search for Python files.\"\"\"\n        matcher = self.matcher(query)  # (3)!\n\n        app = self.app\n        assert isinstance(app, ViewerApp)\n\n        for path in self.python_paths:\n            command = f\"open {str(path)}\"\n            score = matcher.match(command)  # (4)!\n            if score > 0:\n                yield Hit(\n                    score,\n                    matcher.highlight(command),  # (5)!\n                    partial(app.open_file, path),\n                    help=\"Open this file in the viewer\",\n                )\n\n\nclass ViewerApp(App):\n    \"\"\"Demonstrate a command source.\"\"\"\n\n    COMMANDS = App.COMMANDS | {PythonFileCommands}  # (6)!\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            yield Static(id=\"code\", expand=True)\n\n    def open_file(self, path: Path) -> None:\n        \"\"\"Open and display a file with syntax highlighting.\"\"\"\n        from rich.syntax import Syntax\n\n        syntax = Syntax.from_path(\n            str(path),\n            line_numbers=True,\n            word_wrap=False,\n            indent_guides=True,\n            theme=\"github-dark\",\n        )\n        self.query_one(\"#code\", Static).update(syntax)\n\n\nif __name__ == \"__main__\":\n    app = ViewerApp()\n    app.run()\n
    1. This method is called when the command palette is first opened.
    2. Called on each key-press.
    3. Get a Matcher instance to compare against hits.
    4. Use the matcher to get a score.
    5. Highlights matching letters in the search.
    6. Adds our custom command provider and the default command provider.

    There are three methods you can override in a command provider: startup, search, and shutdown. All of these methods should be coroutines (async def). Only search is required, the other methods are optional. Let's explore those methods in detail.

    "},{"location":"guide/command_palette/#startup-method","title":"startup method","text":"

    The startup method is called when the command palette is opened. You can use this method as way of performing work that needs to be done prior to searching. In the example, we use this method to get the Python (.py) files in the current working directory.

    "},{"location":"guide/command_palette/#search-method","title":"search method","text":"

    The search method is responsible for finding results (or hits) that match the user's input. This method should yield Hit objects for any command that matches the query argument.

    Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling matcher. This object has a match() method which compares the user's search term against the potential command and returns a score. A score of zero means no hit, and you can discard the potential command. A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match.

    The Hit contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string. It also contains a callback, which will be run if the user selects that command.

    In the example above, the callback is a lambda which calls the open_file method in the example app.

    Note

    Unlike most other places in Textual, errors in command provider will not exit the app. This is a deliberate design decision taken to prevent a single broken Provider class from making the command palette unusable. Errors in command providers will be logged to the console.

    "},{"location":"guide/command_palette/#shutdown-method","title":"Shutdown method","text":"

    The shutdown method is called when the command palette is closed. You can use this as a hook to gracefully close any objects you created in startup.

    "},{"location":"guide/command_palette/#screen-commands","title":"Screen commands","text":"

    You can also associate commands with a screen by adding a COMMANDS class var to your Screen class.

    Commands defined on a screen are only considered when that screen is active. You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app.

    "},{"location":"guide/command_palette/#disabling-the-command-palette","title":"Disabling the command palette","text":"

    The command palette is enabled by default. If you would prefer not to have the command palette, you can set ENABLE_COMMAND_PALETTE = False on your app class.

    Here's an app class with no command palette:

    class NoPaletteApp(App):\n    ENABLE_COMMAND_PALETTE = False\n
    "},{"location":"guide/design/","title":"Design System","text":"

    Textual's design system consists of a number of predefined colors and guidelines for how to use them in your app.

    You don't have to follow these guidelines, but if you do, you will be able to mix builtin widgets with third party widgets and your own creations, without worrying about clashing colors.

    Information

    Textual's color system is based on Google's Material design system, modified to suit the terminal.

    "},{"location":"guide/design/#designing-with-colors","title":"Designing with Colors","text":"

    Textual pre-defines a number of colors as CSS variables. For instance, the CSS variable $primary is set to #004578 (the blue used in headers). You can use $primary in place of the color in the background and color rules, or other any other rule that accepts a color.

    Here's an example of CSS that uses color variables:

    MyWidget {\n    background: $primary;\n    color: $text;\n}\n

    Using variables rather than explicit colors allows Textual to apply color themes. Textual supplies a default light and dark theme, but in the future many more themes will be available.

    "},{"location":"guide/design/#base-colors","title":"Base Colors","text":"

    There are 12 base colors defined in the color scheme. The following table lists each of the color names (as used in CSS) and a description of where to use them.

    Color Description $primary The primary color, can be considered the branding color. Typically used for titles, and backgrounds for strong emphasis. $secondary An alternative branding color, used for similar purposes as $primary, where an app needs to differentiate something from the primary color. $primary-background The primary color applied to a background. On light mode this is the same as $primary. In dark mode this is a dimmed version of $primary. $secondary-background The secondary color applied to a background. On light mode this is the same as $secondary. In dark mode this is a dimmed version of $secondary. $background A color used for the background, where there is no content. $surface The color underneath text. $panel A color used to differentiate a part of the UI form the main content. Typically used for dialogs or sidebars. $boost A color with alpha that can be used to create layers on a background. $warning Indicates a warning. Text or background. $error Indicates an error. Text or background. $success Used to indicate success. Text or background. $accent Used sparingly to draw attention to a part of the UI (typically borders around focused widgets)."},{"location":"guide/design/#shades","title":"Shades","text":"

    For every color, Textual generates 3 dark shades and 3 light shades.

    • Add -lighten-1, -lighten-2, or -lighten-3 to the color's variable name to get lighter shades (3 is the lightest).
    • Add -darken-1, -darken-2, and -darken-3 to a color to get the darker shades (3 is the darkest).

    For example, $secondary-darken-1 is a slightly darkened $secondary, and $error-lighten-3 is a very light version of the $error color.

    "},{"location":"guide/design/#dark-mode","title":"Dark mode","text":"

    There are two color themes in Textual, a light mode and dark mode. You can switch between them by toggling the dark attribute on the App class.

    In dark mode $background and $surface are off-black. Dark mode also set $primary-background and $secondary-background to dark versions of $primary and $secondary.

    "},{"location":"guide/design/#text-color","title":"Text color","text":"

    The design system defines three CSS variables you should use for text color.

    • $text sets the color of text in your app. Most text in your app should have this color.
    • $text-muted sets a slightly faded text color. Use this for text which has lower importance. For instance a sub-title or supplementary information.
    • $text-disabled sets faded out text which indicates it has been disabled. For instance, menu items which are not applicable and can't be clicked.

    You can set these colors via the color property. The design system uses auto colors for text, which means that Textual will pick either white or black (whichever has better contrast).

    Information

    These text colors all have some alpha applied, so that even $text isn't pure white or pure black. This is done because blending in a little of the background color produces text that is not so harsh on the eyes.

    "},{"location":"guide/design/#theming","title":"Theming","text":"

    In a future version of Textual you will be able to modify theme colors directly, and allow users to configure preferred themes.

    "},{"location":"guide/design/#color-preview","title":"Color Preview","text":"

    Run the following from the command line to preview the colors defined in the color system:

    textual colors\n
    "},{"location":"guide/design/#theme-reference","title":"Theme Reference","text":"

    Here's a list of the colors defined in the default light and dark themes.

    Note

    $boost will look different on different backgrounds because of its alpha channel.

    Textual\u00a0Theme\u00a0Colors \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Light\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

    "},{"location":"guide/devtools/","title":"Devtools","text":"

    Note

    If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.

    See getting started for details.

    Textual comes with a command line application of the same name. The textual command is a super useful tool that will help you to build apps.

    Take a moment to look through the available subcommands. There will be even more helpful tools here in the future.

    textual --help\n
    "},{"location":"guide/devtools/#run","title":"Run","text":"

    The run sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app.

    textual run my_app.py\n

    This is equivalent to running python my_app.py from the command prompt, but will allow you to set various switches which can help you debug, such as --dev which enable the Console.

    See the run subcommand's help for details:

    textual run --help\n

    You can also run Textual apps from a python import. The following command would import music.play and run a Textual app in that module:

    textual run music.play\n

    This assumes you have a Textual app instance called app in music.play. If your app has a different name, you can append it after a colon:

    textual run music.play:MusicPlayerApp\n

    Note

    This works for both Textual app instances and classes.

    "},{"location":"guide/devtools/#running-from-commands","title":"Running from commands","text":"

    If your app is installed as a command line script, you can use the -c switch to run it. For instance, the following will run the textual colors command:

    textual run -c textual colors\n
    "},{"location":"guide/devtools/#live-editing","title":"Live editing","text":"

    If you combine the run command with the --dev switch your app will run in development mode.

    textual run --dev my_app.py\n

    One of the features of dev mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.

    This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.

    "},{"location":"guide/devtools/#console","title":"Console","text":"

    When building a typical terminal application you are generally unable to use print when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores print and adds a few additional features to help you debug.

    To use the console, open up two terminal emulators. Run the following in one of the terminals:

    textual console\n

    You should see the Textual devtools welcome message:

    textual\u00a0console \u258cTextual\u00a0Development\u00a0Console\u00a0v0.44.1 \u258cRun\u00a0a\u00a0Textual\u00a0app\u00a0with\u00a0textual\u00a0run\u00a0--dev\u00a0my_app.py\u00a0to\u00a0connect. \u258cPress\u00a0Ctrl+C\u00a0to\u00a0quit.

    In the other console, run your application with textual run and the --dev switch:

    textual run --dev my_app.py\n

    Anything you print from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application.

    "},{"location":"guide/devtools/#increasing-verbosity","title":"Increasing verbosity","text":"

    Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as \"verbose\" and will be excluded from the logs. If you want to see these log messages, you can add the -v switch.

    textual console -v\n
    "},{"location":"guide/devtools/#decreasing-verbosity","title":"Decreasing verbosity","text":"

    Log messages are classififed in to groups, and the -x flag can be used to exclude all message from a group. The groups are: EVENT, DEBUG, INFO, WARNING, ERROR, PRINT, SYSTEM, and LOGGING. The group a message belongs to is printed after its timestamp.

    Multiple groups may be excluded, for example to exclude everything except warning, errors, and print statements:

    textual console -x SYSTEM -x EVENT -x DEBUG -x INFO\n
    "},{"location":"guide/devtools/#custom-port","title":"Custom port","text":"

    You can use the option --port to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default:

    textual console --port 7342\n

    Then, use the command run with the same --port option:

    textual run --dev --port 7342 my_app.py\n
    "},{"location":"guide/devtools/#textual-log","title":"Textual log","text":"

    Use the log function to pretty-print data structures and anything that Rich can display.

    You can import the log function as follows:

    from textual import log\n

    Here's a few examples of writing to the console, with log:

    def on_mount(self) -> None:\n    log(\"Hello, World\")  # simple string\n    log(locals())  # Log local variables\n    log(children=self.children, pi=3.141592)  # key/values\n    log(self.tree)  # Rich renderables\n
    "},{"location":"guide/devtools/#log-method","title":"Log method","text":"

    There's a convenient shortcut to log on the App and Widget objects. This is useful in event handlers. Here's an example:

    from textual.app import App\n\nclass LogApp(App):\n\n    def on_load(self):\n        self.log(\"In the log handler!\", pi=3.141529)\n\n    def on_mount(self):\n        self.log(self.tree)\n\nif __name__ == \"__main__\":\n    LogApp().run()\n
    "},{"location":"guide/devtools/#logging-handler","title":"Logging handler","text":"

    Textual has a logging handler which will write anything logged via the builtin logging library to the devtools. This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs.

    Note

    The logging library works with strings only, so you won't be able to log Rich renderables such as self.tree with the logging handler.

    Here's an example of configuring logging to use the TextualHandler.

    import logging\nfrom textual.app import App\nfrom textual.logging import TextualHandler\n\nlogging.basicConfig(\n    level=\"NOTSET\",\n    handlers=[TextualHandler()],\n)\n\n\nclass LogApp(App):\n    \"\"\"Using logging with Textual.\"\"\"\n\n    def on_mount(self) -> None:\n        logging.debug(\"Logged via TextualHandler\")\n\n\nif __name__ == \"__main__\":\n    LogApp().run()\n
    "},{"location":"guide/events/","title":"Events and Messages","text":"

    We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.

    "},{"location":"guide/events/#messages","title":"Messages","text":"

    Events are a particular kind of message sent by Textual in response to input and other state changes. Events are reserved for use by Textual, but you can also create custom messages for the purpose of coordinating between widgets in your app.

    More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.

    "},{"location":"guide/events/#message-queue","title":"Message Queue","text":"

    Every App and Widget object contains a message queue. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line.

    Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right away.

    This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive.

    Tip

    The FastAPI docs have an excellent introduction to Python async programming.

    By way of an example, let's consider what happens if you were to type \"Text\" in to a Input widget. When you hit the T key, Textual creates a key event and sends it to the widget's message queue. Ditto for E, X, and T.

    The widget's task will pick the first message from the queue (a key event for the T key) and call the on_key method with the event as the first argument. In other words it will call Input.on_key(event), which updates the display to show the new letter.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9zq+g2C97q4Iyj57XVm3dXG6Eh8NcdTAwMWJCSPbuXHUwMDE2JWzZViw/sGRcZknlv99cdTAwMWVcdTAwMDGWLEu2McaYutdcdFx1MDAxOEujUVvTp/ucnpF+rqyurkV3XHUwMDFkb+2P1TXvtuxcdTAwMDZ+pev2197Z7TdeN/TbLdzF4s9hu9ctxy3rUdRcdP94/77pdlx1MDAxYl7UXHTcsufc+GHPXHLCqFfx20653XzvR14z/Lf9feg2vT877WYl6jrJSda9ilx1MDAxZrW79+fyXHUwMDAyr+m1olx1MDAxMHv/XHUwMDBmfl5d/Vx1MDAxOf9OWVx1MDAxN/gtL25cdTAwMWJvTWzjhGW3XHUwMDFltluxnYxpJYFcbjlo4IdcdTAwMWbxTJFXwb1VtNZL9thNa3ew7a9v1GuNw6P92uXn453d/r5Mzlr1g+Asulx1MDAwYu4vgluu97opm8Ko2254XHUwMDE3fiWqP16z1PbBcVx1MDAxNTeso1x1MDAwMYPd3XavVm95of3udLC13XHLfnRnt1x1MDAxMTLY6rZqcSfJllv8XHUwMDA0TDlcXFFiuFx1MDAxMoNcdTAwMWT2UC7AoZRRZncxo1x1MDAxNM9cdTAwMTi12Vx1MDAwZXBcdTAwMDTQqN9I/EqsunLLjVx1MDAxYZrWqlxm2kRdt1x1MDAxNXbcLo5T0q7/+HVcdTAwMWRJQSpcbmBwREApXHUwMDE4NKl7fq1cdTAwMWVZczRzXHUwMDE4lZRcbs2pXHUwMDAyloxL6MVDXCK0XHUwMDA2LiE51FrQKVVit/gne0HrbrfzcN3WYktT1tuPWymfSlx1MDAwZe51Ku69XHUwMDAzUCk5XHUwMDEzaCmXLLmg6GdcctzZ6lx1MDAwNUGyrV1u5PhMXHUwMDE4ud1ow29V/FYte4jXqlx1MDAxNOxcdNww2mw3m36EZlx1MDAxY7f9VpRtXHUwMDEx9/uh2233655byem5cF/HdpeAyL6Sv1ZcdTAwMTO3iT9cZv7+511u6+Ihta+RwUy6W0m//3r3RDxLkt36iGdcdTAwMDVUXHUwMDEyTVJccibhueyT7cMvnXWz8dm/8b5cdTAwMDU7p+ftT0uPZ0Ul4llcdTAwMWJcIllcdTAwMTbP3MGQxlx0XHUwMDA3/Mf0y8HZOIRxjfiQglBcbjRcdTAwMGbNyjhMMcE5XHUwMDA1wVjyVVx1MDAxZsBMNTWMS6lcdTAwMTKY/1x1MDAxZs6vXGLnwiG1r+xgPlx1MDAxMcxdr1x1MDAxY937clx1MDAwZaKFVEWIplxuXHUwMDA3jFx1MDAwYlx1MDAwNXpqSJ9/b1xis8v5XHUwMDA1bFx1MDAxZVx1MDAxZV1Hd5dHSjZngzTNuuDjcWFcdTAwMWIpylxcMzRwR2mBVCSDaClExognYfg3KEuvKlx1MDAwMHJcdTAwMTKyTK7pXHUwMDAwszpcdTAwMTXFXHUwMDFlUMqBMMFcdTAwMDQkXHUwMDA2z1xypVx1MDAwMyf6mfK0gc9E3m1cdTAwMTJ4Ulx1MDAwM1xcPWq4ncjb0Xe1T0dbknxcdTAwMDH6w19cdTAwMWK0+/Uuv9v7gzfV0Y3r9Xav3fN+dHHEXHUwMDBmw8Nob/gsj+d3LepS/T46+lx1MDAxY0PLWMxcZn3/NFxcaCFcXDDVcmGQ1k6NlvyLufRo0Vx1MDAwNWjR4DxcdTAwMGIv4ymsyEFcZuNZxFx1MDAwMKXKMMlnSGuh/bDotFZtt6Iz/0csiMjQ1m236Vx1MDAwNzGvXHUwMDE4bI6d0mrBXHUwMDFia5Oz59393vDu/vx77fPfa/9KX9rQi1x0XHUwMDFj2meGXHUwMDBl/lx1MDAxMPi1Vky9sFx1MDAwM6875OCRj9pv0KDpVyrphFFGi1xc7LNbmibOt7t+zW+5weexXHUwMDA2z560kPNcdTAwMTYmLcGUXCKYJNnUMCTVjdb2xkWTXHUwMDFk7PbPvm9dnVx1MDAxZJWbfPlhSFx1MDAxZKo5Q6bJjaaSXHUwMDBmYVFcYuEg61dcdTAwMDb5qFx1MDAwNinUc3A5hzymKCVcblxy0U+H5Wx57H5kw89cdTAwMDcnXHUwMDFmLjpy0z24U7J1XHUwMDFhfe3fVvNcdTAwMTNOjK2JeWxSesw/4fKlMfSeQlx1MDAwMCHTXHUwMDA0LZWeXsiNv8xLXHUwMDBiIDlcdTAwMGVAmMzMvFx1MDAwMDSPxEa1llx1MDAwNNW+nIFcbr7hzOYtOrNNSFx1MDAwNlx1MDAxMzObNzGz3VPbXHUwMDFjUFwiqSpcdTAwMDIlXHUwMDAwXHUwMDAzxSVMXHLJ8VR7SSEpNHU4IZwrKrVG2TNcdTAwMDRJhTnN4GZNWTHNxGtULavx6azqoqRiUo5iXHUwMDExUypcdTAwMDDmKq2I1Pa/XHUwMDE4hSaAY00kWnMlXHUwMDA1g5HSikBcdTAwMWLwXHUwMDFi0DdaWUny3WPdf1x1MDAxYcpcdTAwMTdDu9yzVq5cdTAwMTNcdTAwMDdcdTAwMDNcdTAwMTWOlK3uXHUwMDBiW1xiXHUwMDAzwVPtam4njmeDwXzYNci5w/WcXCJ7dvrkR61xcvDtU/PM6NBcdTAwMWNdnlx1MDAxZbM8e9BcdTAwMWPCLP1AUqRMbFx1MDAxNchcdTAwMTF7XHUwMDE4sVU9Jlx1MDAwNGZcdTAwMDKQXHUwMDFj4/6IWXMuJmVcdTAwMDPBXFzrScWebF8jPpx0t5J+n4mcayOzW1x1MDAxM26BmVRSdIqpXHUwMDAz2aHZuNBcdTAwMDY2TrpcdTAwMDem8uV6//r6Q+922Vx1MDAwM1x1MDAxOVx1MDAwMHFQgiB70KhGbVx1MDAxMW0okiFcdTAwMWVcdTAwMWNFiFx1MDAxMdxcdTAwMDBcdTAwMGVcdTAwMDFJz7W8XHUwMDBlPVx1MDAxN1x1MDAxOHpcdTAwMTFcdTAwMWJcdTAwMGIrM92Prdzp175931xc3708//bps/6q6mqDPYeev1C3k1h//lx0p7T2W2f/ekdvVIOvXHUwMDE3f7ntvV23tHnWfltFMVxyxTVkQe0sr2HTU5fxw7e0iFx1MDAxN2NcdTAwMTGvucPmhvh56Fx0rVx1MDAwNDpH+ov9L8iJ20XLiVx06WuinLidKCdcbivVKchlQGlcYqXUwPRcdTAwMDJfbrPaycc72DpcdTAwMTW9nql978nLzt2yQ1JcdTAwMTnioGRcdTAwMWWpU1x1MDAwYtxupFYvNbeD3DdcdTAwMDeAXCJcdTAwMGJANEzb4MhcdTAwMTZcXFx1MDAxM1uufPNcdTAwMTR80yGIWlx1MDAwM1x1MDAwZvB6uDVv9brn9bx8XFzroYNcdTAwMDawXHK8ajRcdTAwMDbVUbtTXHUwMDA06aEvk8XvsEFjcVtYXHUwMDA2oHRMOqXcIJtcdTAwMTdPwO748V5S7GqgXHUwMDBlXHUwMDAwXHUwMDE4SbmmXFyZXGaCNThcZlx1MDAxOH1BXGZT7lCFSoZcYsZcdTAwMDQjyYBcZlx1MDAxMG2UwzhcdTAwMDGJTYhSSo+sl1x1MDAwMkzzgmj2XHUwMDAypPo161x1MDAwMONzwWq2XHUwMDBlgNFXgWGUaaF5stZg9VF3S0ehKJSzVlx1MDAwMcbn12FrcDhccjNKcCEwL+SWXHUwMDAwXGJcdTAwMDAj2MagXHUwMDFlXHUwMDAypkdsekslgPVCJ473jvhv0t9K+r0oflV8t9lOe2lqdkFcdTAwMTSuXHUwMDEyY4Rb5snp9Ms+x1x1MDAxN3qWNIBh8HK0XHUwMDE0dnEnXHUwMDE1SpiMXHUwMDFlwKjgMGUwglx1MDAwMTJcdTAwMTHgJmPY0+LYfUEzt1x1MDAwMKByuFxiZcaRoCG9IPUhZiFOXHUwMDE5oF2zyILnkJJnrFxmeZff7yTNLi5PXCLYgNOrvd3u7dFx+aJeOjmeVrP/pc7Pt672zlx1MDAwZvC6d3+cd8PN2+rB/DiUVjzh7i+k2XnxQlx1MDAxNiaBaOtPU0M0/2IuPUTNWIgqjvKBXHUwMDAzkZZoPFx1MDAxN6LjanR5cmF0/lx1MDAwZrRWXHUwMDEyg/lbWX89k2Bvty5R9/5cdTAwMWXL4MVcbvVcdFkmS/SHXHKdXHSBjFx1MDAxNM/BI/9QWpjpk+TJlemTm6PNXHUwMDEyLdPti7vatquDcNlcdTAwMTGokIMgK2RcXFKquExcdTAwMTWm4/k+TVx1MDAxZGOsXG5cdTAwMDDQ3OiXypGU58zyjep1ZVxiVUy/XHUwMDAw/F4x0yxUrW9ZsKzW3Vx1MDAxNuKwm1x1MDAwZu7FqvVhg8aCuFCtKz6O6zKM2IRNL9bHXHUwMDBm97LCWHNcdTAwMDcpP1hcdTAwMWOPwlhcdTAwMTPqXGKhha1+XHUwMDEzolPMZr4wNlxmo1x1MDAwNWpNlN2a4agksXNcdTAwMDBqQVx1MDAxZFwiXHUwMDA0xWCj8Vx1MDAwN1LceLDIRtgpU/1cdTAwMDIludeU7OOTw2p6qlxcoTI2wOyEoFxyeCzV6GHeXHUwMDFlXHUwMDFjylx1MDAxNzNtz/GyKM5cdGpx1KlKiFFjhMM1XHUwMDAwupZUWinJzYhRb0qxXHUwMDE3+rB9jXhv0t1K+n2mSXtKivVcdTAwMDBcdTAwMDVcdTAwMTTtXHUwMDA0zZk+jqldz5elXm1cdTAwMDM2LkNZ+Ytz9/Ro2eNcdTAwMThyfMdII6XAK0+0XHUwMDFjjmNcXChcdTAwMDe4jitIhEry2ktqhTBcdTAwMDQlXHUwMDAxWfD0Qalx2bioXHUwMDA2/CvtnqhcdTAwMGa3lZ0621x1MDAwZZ4/Z/9Wup1UVsg/4YuSsrGgL1x1MDAxMlx1MDAxZkZcdTAwMTXSXHUwMDE2VJqc2rtKp7+RZfxlXla4XHUwMDAzjIO7lo6ZXHUwMDE33OcxYc+IXHUwMDE0KJhmwftcdTAwMWKesI9cdTAwMTY9YT8hc02csI+ed2eLLr6zhVx1MDAxOUzDVlx1MDAwZk9ccstcdTAwMDNSql5cdTAwMWNdRKWQ7ZQvvG+eOP9SKoBludtcdTAwMGXD9bpcdTAwMWKV669cdTAwMGZNpHNcdTAwMGUyPs24Xb+YWmJcdTAwMWJ7XHJoh1x1MDAxMW0pIVx1MDAxNUSl65TzhqZQXGbZruFcdTAwMDaZJSbr1PrsoVx1MDAxYq1cdMdcdTAwMTbI2URcXLdcdTAwMTlcdTAwMDUuoFx1MDAwZeTmXHUwMDE1bkmjT1x1MDAwMO7sPitk4UM+7O3wXHUwMDFhuez0qaTSb7tl3W5tXHUwMDA3VyGr1Fp998dOUSrJ+N2Uj1x1MDAwNHhcdTAwMTFvxcG3t0pcdTAwMWJOhcy6q33+XHUwMDA3XHUwMDExhkrQRKpcdTAwMTdcXPolXHUwMDE0cVx1MDAwMKhRQJhGRZ6TVzhx7FJoSbhBXHUwMDExh/pqhFcqXHUwMDAzXHUwMDAyMM+8wsKwubmrXHUwMDE3XHUwMDA0fifMd1ZerHNcYlx1MDAxN1ZdmelXJ1+VTndcci25n8pcdTAwMDErl1x1MDAwM6M/qkZvXHUwMDE2b11gbMWAhY5cbkQwpDU8VVa9VzlcdTAwMWElNXqytM8jXHUwMDAwMY9qTd5cdTAwMTIphyiD0TteXHUwMDFmQSCnVkPtgklbXHUwMDE2tus8XHUwMDE4KtBRXHUwMDA1JCVSXCLD2VxmJdk34ammcH5AKo5xRD1hXHUwMDEyvXlrbrauv3/8us+jq1x1MDAxYrfqXHUwMDFk7+1cdTAwMTXd47okjootlIP5X2AsI4qAyD5uRWEoI1xc2ck5JVKqeTZPvVwiRLyQp1x1MDAwMrpcdTAwMGXG2te4dW9cdTAwMTGOKkFcdTAwMTQ5KqPx04fQW6f21HOXlEo/tpplJk5O9XW9XFzz5e6yeyrHXHUwMDFjT1x1MDAxOUFMYlx1MDAwZeFcIsNcdTAwMDCEdOzDkYjUklx1MDAxMmDPm8ii7ErrvHvX5lx1MDAxMlNcdTAwMDHiqDpLXHUwMDA1fGlcXHX8o1x1MDAwM3Rx4UNrJGmM8enDqiq3dvfDm1x1MDAxZF1vymr3MvhQqvpmtsLH4vgqRUnlUKGZNMo+XHUwMDA3avg2S8GIozVFSYNcdTAwMTRcdTAwMDFkXHUwMDE2RfNjq9xcdTAwMTBHWSdcdTAwMTSA0VvTvFx0XHUwMDFi7iBcdTAwMTNcdTAwMDVqn+oh7TrQrL9yaiTmhtd43Fx1MDAwN1+Iu45Zxc9cdTAwMDE4t3Cd2lt31zuNnb1bV1x1MDAxZl/pfr9zXHUwMDFjnFx1MDAxZpr9ZVx1MDAwZq2KO0rZKVx1MDAxMeuvOlWHi52VY9RcdTAwMDP0JFx1MDAwZZpcdTAwMTIqnuWtXHUwMDBmRfmc0KpcdTAwMWNcdTAwMTR3qGONQHVl1ySPuirTjp2HQkuJoExcdTAwMTgyqqwk2FuDXHUwMDA0vEJsfbqzrjxcdTAwMTSp19xO5yzCLtdcdTAwMWXn9NBqv/JQ2Uu6Wbvxvf5GXHUwMDFls4pftiRcdTAwMTZcdTAwMDPAeplnbf75a+XXf1x1MDAwMfpa2G0ifQ== events.Key(key=\"T\")events.Key(key=\"e\")events.Key(key=\"x\")Message queueon_key(event)Event handlerevents.Key(key=\"t\")

    When the on_key method returns, Textual will get the next event from the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an idle state.

    Note

    This example illustrates a point, but a typical app will be fast enough to have processed a key before the next event arrives. So it is unlikely you will have so many key events in the message queue.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPbOLb93r9cIpX+8l5Vi1xy3Fx1MDAwYlxcXHUwMDAwUzX1yrvlxI5cdTAwMTfF25splyxRiy1LskSvXf3f50JxLGohJVlL5ExUldgmaVx1MDAxMCbP3Vx1MDAwZi7++u3Dh4/RUzP8+I9cdTAwMGZcdTAwMWbDx0K+Vi228lx1MDAwZlx1MDAxZv/wx+/DVrvaqPMp6Pzcbty1XG6dKytR1Gz/488/b/Kt6zBq1vKFMLivtu/ytXZ0V6w2gkLj5s9qXHUwMDE03rT/z/+/l79cdP/ZbNxcdTAwMTSjVtC9SSYsVqNG69u9wlp4XHUwMDEz1qM2j/7//POHXHUwMDBmf3X+j82uVq2HnWs7R7tzU6D7j+416p15XCKR4tOye0G1vc53isJcIp8t8WzD7lx1MDAxOX/oY+FY1pu721x1MDAwN42709rGTqV1+bTXyHTvWqrWakfRU+3bQ8hcdTAwMTcqd63YnNpRq3FcdTAwMWSeVItR5fszi1x1MDAxZH/9vWK+XeFcdLyebjXuypV62PZ/u3w92mjmXHUwMDBi1ejJXHUwMDFmXHUwMDEz4vVovl7uXGbSPfLIP2VcdTAwMDBFYFx1MDAxZFx1MDAxOaW1QjSWXk/7XHUwMDAxMnxSKyc0WWONUYawb25rjVx1MDAxYb9cYp7b76Lz6U7uMl+4LvNcZuvF12uiVr7ebuZb/Lq61z18/6tcdTAwMDOSioxUyqFcdTAwMDDFd3u9pFx1MDAxMlbLlci/XHUwMDE2XHUwMDBiXHUwMDAxSJJSW5RGQXe27bDzZow2xlxup8zrXHQ/hWa22IHHv/tcdTAwMWZsJd9qvjy/j52pxqbvf9yIYav7y3fNYv5cdTAwMWJcdTAwMTAkXHUwMDExgjZcdTAwMWFcdTAwMTGlfT3PeLvmk/W7Wq17rFG4XHUwMDFlgp12lG9Fq9V6sVov9/9KWC8mnKnl29Fa4+amXHUwMDFh8TT2XHUwMDFi1XrUf0Vn3JVWq/FQXHTzxSEjJ55r+uG6wuQ/3e8+dOHT+eH1+3//MfTq5HfqP1x1MDAwM2+zO9xv8a9//zGZXFzHxbZPrqU1XHUwMDEy/VvrXHUwMDAyZJRgXHUwMDE3XHUwMDFmt+uHNbm/cqLXilLKp1P8urX0gk1cdTAwMTiAQaeVllx1MDAwNqxm+e5cdTAwMTVsXHUwMDE5KFx1MDAwM1JoaYGUk6J/arOTa1xmLFx0x4IpXHUwMDE0KiMsdoW2K9fGXHUwMDA1pNFJLdFcYsDYbL7LtZaSQHfVzy+5/oFynfxOO2f73+aEct1cblx1MDAwYtE3VFx1MDAwZlx1MDAxMW5cdTAwMGKq/+h34TYgyFkruiBcdTAwMTkl23pcdTAwMTez6vhuf0tcdTAwMWWXXHUwMDBln45WN6urXHUwMDBme2+TbdmPwe+/126w0zJbmy1VQGCsklpb/qe74uJHXHUwMDAwZ1x1MDAwMudcdTAwMDRcdTAwMTFYh85p0zeziUT7d1WgsKSVXHUwMDFhYrCpK1x1MDAxYa+SbFx1MDAwN0TXXHUwMDE5XCJWMNrNXnRfcfVXXGZ9L6/2+jS7cnZcdTAwMWGFZ1x1MDAwZW9Xwp2zbPSw1vxcdTAwMTiH6SveovAx+vh64u8/foZhe67+Y9xcdTAwMWJ2h/0uqDNUjqky3zPPmLhcdTAwMWIjksRdXHUwMDFhUsiuKcmx5T39MS+tvFx1MDAwYpcm71xiJlCzkvd0XHUwMDE3XVx1MDAwZpF4wH6JZ/3EXHUwMDEzVSrmc4wt8m3/w6KtdalRj46qz/7Rg+g5upm/qdY6XHUwMDBm+fVwXHUwMDA3qT7mvfdzXG4+hU//c1x1MDAxZD79819cdTAwMWbDf3383/izbYdcdTAwMWRcdTAwMDeV5+d6fnmlVi3XOyEjXHUwMDBmXHUwMDEwtnpQXHUwMDFmVTnGfb3gplosxq1ggWeU5zFb2XGsV6NVLVfr+VoudcJvt8RSOpckmyDY9+T34cZcdTAwMGagm0BQyX6y95VTKp5+NjuGtnaWXzalXHUwMDBljJLkUKFjV1x1MDAwN/pssVCBs8ZZJVxmsG+ippHN6W0xsVtG1mo5uWBOY4tLXHUwMDE2Tyh7sEVCXHUwMDFlZTZcdTAwMGLbUT17tT290fwvXHUwMDFmdpSJXHUwMDFmfsPlM/FSxtI1/TZeXHUwMDE44jBC4vg+ffpzXl49XCJS9VxiqkDNSo/MxMZrhWTi7+W/wcQ/LtrEjzCKI03845QmXHUwMDFlY2++XzSVcFx1MDAxY+lrM777fWLv7k5LVXyqf3Lbd+XqMUVN/Vx1MDAwZURTXHUwMDA11pJBpYXSZGLa6ttcYi4wXHUwMDE2vOdttXFcdTAwMTLtNLI5vY2X6KQlqWBcdTAwMGU58DT7tpYt3lZutrPQus3kSoXyVrNcdTAwMDJcdTAwMGbTm81fw85j2FG+w/BcdTAwMWIuoe9cdTAwMDAmMVx1MDAxZFxiXHUwMDAyyLDcqq5uXHUwMDFlpaDSn/PSKijWQGlcblxuXVx1MDAwMDNTULNwXHUwMDFlkDg45Hm8ISX4jp2HaNHOw1xiczvSeYimdFx1MDAxZYSEJNmUvqpsSajx81x1MDAwM6JcXCg+fd543nzOrZ6a1np0YrYuXHUwMDEzZLPQarTbmUo+KlQmrsXNXFw+Ob5cdKRcdTAwMDGSTrGEOid7xVNcdTAwMDdWXHUwMDBiw+60clx1MDAwNlx1MDAwNcr5pe/Yq1xiWFx1MDAxMtBnTaWMXHUwMDE1Sbt1OFx1MDAxMoFlh0YpQuzUXHUwMDA2+0WXWMU4K/Fcclx1MDAxOYRpRJescjSB6L5cdTAwMWS1XHUwMDA2XHUwMDEyXHUwMDEzzmA58lwimKB2/PDl7Ph+t+lobbN+vXffzl2VaDdcdTAwMDGzfbj7cWjFwFxuzViUgpW0xD6wXHUwMDAyaTYhPi5nXHUwMDE42XmCXHUwMDE1XHUwMDAzIUn74rU0zlxmMyw2IFx1MDAxMqhYj1x1MDAwMCFcdTAwMDNlXHUwMDAwrdJcIlnygcpyW5pUuIa1WrXZXHUwMDFlXG5W0olMXHUwMDA3p4W39+P7Puu3bq0sXHUwMDFh1fUoV6GV+pdqWTxfvVx1MDAwNauL83wk6MBcYumENlx1MDAwNlx1MDAxND952Vx1MDAwM1apXHUwMDAzjUaQY8xqXHUwMDA2K01Fcvi9lNegYVx1MDAxMKksMEpcdTAwMDBoR+xxgoonZ16hKiHQRMqxUlx1MDAxNYBcYjFcdTAwMTftO1RcdTAwMWRJY1x1MDAxNDtoPydUjUgsXHUwMDE2+EJcdTAwMDJpJeT4Sb6TxtZOY2vvYHf/5jh7lFx1MDAxN7lneX605GDV4K1vJzRX6ND2eekqQKk1Y4RcdTAwMDVcdTAwMTdcZsXM3dvAeimEnlx1MDAxYljRSkOCZe8nXHUwMDA1KyWnvdjYgEOYgGWSoULrRu+F1fPTs8v80VWrjodPS1x1MDAwZVYrXHUwMDAy0pbh4Yz3XHUwMDA2bS9WkVxyL1tV7SyHcYyT6VJeXHUwMDEyLtmvmlx1MDAxN1ZZliSHxPie9Wqqx0o6UbNqUFx1MDAxMlmnjFx1MDAxZmWd5MzVYSV/U9k938k837ab1y6iXHUwMDA0rL6V7ThztIKgXHUwMDAwpFJAXHUwMDA2vVbqJTFLclx1MDAwMTgvs6y1PMV5jlxcRyNcdTAwMDPHYZZX4GRUTD2+XHUwMDAyVstcdTAwMDDNNzWPXG6MjnHQvytX45xcdTAwMDWcS23l5cTQJOP9cVbVVm+fNr82M/fb59tfT7ZKZ/NMMlx1MDAwZb9hd9iX7354klx1MDAxMZO9bClYN1x1MDAxMlx1MDAxODF+TJj+mJc0x1xiUqZJmKFAWWG0L1xisveC81x1MDAwYlx1MDAwYpFGSlx1MDAxOFx1MDAwZZhcdTAwMDBcdTAwMDaLs07rd8hH8sszXHUwMDE4Y2jIXHUwMDAya1x1MDAwYqN7LkpJP+aGplx1MDAxYVH0XFz4mkmshaVe6E+WaEw3XHUwMDFh/YnG3Fx1MDAxNElFJWWSLJJcdTAwMDEhrFx1MDAxNuOHvMeZ6PK2urlXOz0pn12VVk4zN/nscjtmLFuBJasth6LaOWN7ef3SYoDs7CiUgFx1MDAxY/xOtVxc56VcdTAwMTY5xDEzgWGnT7BMKVx1MDAxZrfi8OSMXHUwMDEywnFIKzTH6U5cZjpmyidtOEpZbqlMxWryXHUwMDEylORo11x1MDAxYZRKqFx0XHUwMDAyiIddsV68P195PG89Xrebh1FUeWjN2Cmby9oy1tVWO4UgjZZcdTAwMWPi92BVkVx1MDAwZZBDXGb5Ld6FfiFasqVlUmhfTsS3xLy/1qB0zv1cdTAwMDRryyhZsI2w1lx1MDAxOFx1MDAwYuPzYS42L53UT7BfrFx1MDAxZcv1c7neLlx1MDAxZp4svVxcXHUwMDEz+Fx1MDAxNaPgWIU5xyFMb9LVSzU/dDZR5Dx+xfyoalx1MDAxOEjl2CfloE5rXHUwMDAwXHUwMDE4YoX8UiQgskpY9lx1MDAxNy1cckg1O1fOL2Waw+qUX0LtP5MuLEt4pZ2T/S9zQplcdTAwMWVBdVOJnqVcdTAwMTRoXHUwMDA1mHh9YZRor+7fXVx1MDAxY8r26flDTWy3rlx1MDAwZlx0sVx1MDAwMctcdTAwMWXlZVhjXHUwMDA2noPKVpmkYC+lN84z2lx1MDAwNb6K7UChXHUwMDE22s3Cu5yG6SZYs/ty5IKZboXWYeZi5XRtu1x1MDAwNNW7jWN+zc/H+9OTvH5ccjuPYUcloYbfsDvsd82yqIhcIpnpZm3/4S6bXHUwMDA2lNPG2lx02lWkPufl1U8yVT9ZXHUwMDFi0Kz00yyIbpI9VH415i39KH505uk90eRHWNu50+S1SF7B4pRcdTAwMTakXHUwMDEwx09LRe58y17WWpXN58+5q0+5i82tKImJsUyyyY4/kSSQVkiUvYkpPlx1MDAxMXSWt4DlmExN10dmetfBXGKNPnhcXLDnXHUwMDEwfn740l5dfb4o7aysRcdcdTAwMWKHub3bcHqj+WvYdzTsKIdk+Fxyl9AhUTpR6ZGViJMszE9/ysur8kSaynNcInCzUnkz8Ua01Vx1MDAwNMLJJc+4z9hcdTAwMWJZOO9+hP2eO+9cdTAwMWUokXfv0Fx1MDAxOHJajC+ZT1dP5d3jXG6gK2Tz+0dcctkw2fz7oN3rXHUwMDAwkP1cdTAwMDF2v1x1MDAxZItnf57SYEBWKOUkkmDJmF+eUmtcdTAwMTlcYoNgXHUwMDEwWVx1MDAxM0DM6YhcdTAwMTHvNV9iXHUwMDFjkTZcdTAwMWPokVx1MDAxOFJ+UKCs027BoYRcdTAwMDGn1Fx1MDAwNML7dty6WGewfthKUlx1MDAxYeI07pFd29a/NL9cdTAwMWWph3ZOZa5lTX6+XFy/uFh25r3HXHUwMDAwaWLAcrTvpO1cdTAwMDMsXHUwMDA0YKywXGZcdTAwMDGNen7VMq1cXKDQovJcdTAwMGLGKL6ONF7cdc5cdTAwMDFbXHUwMDEyy3hGUv1oRY7OeziSXHUwMDBiwapcdTAwMDJQM1slkkJcdTAwMGa1NpFcdTAwMWVcboJDPclcbmV8XHL7aaWZrZePi5vnj7uVtdrGp690t+QsXHUwMDA0NjGBdVx1MDAxNlx1MDAwMY10IJXp5TIri1x1MDAwMVx1MDAwZqJQa4NcdTAwMWNo4bRcXOZE4r0mzzpSgr84I4d0XHUwMDE2lFx1MDAxMFx1MDAxOKklq1/tXHUwMDFjuXjrk1dcdTAwMWFcdTAwMDKAdKhcdTAwMTesV1x1MDAxN4VVh8mpXHRcdTAwMTZRYmtcdTAwMDPjpybOro4q2Vx1MDAxM1co1Vx1MDAwYitcdTAwMDeZ3U27+1x1MDAxNZeed69cdTAwMDKPUCNAsONDivqwKlx1MDAwM9ZyymotSKrp8lx1MDAxMims+1x1MDAxOSCVpc5cdTAwMTH/XHUwMDE1uGD/fWFQdYlcdTAwMTU4VEJIXHUwMDAxMD7N8su5OVxcKa1cdTAwMWPU90pfi632Sq7cLN4tOVItXHUwMDA0WmhALaxfx1xyXHUwMDAzQFx1MDAwNb8kz1x1MDAxYlxcS1O2Yk4j3c9cdTAwMDCqRiqrLL2BKrM0QFx1MDAxZOGrJrZcdTAwMWSQmpBgolx1MDAxOOvgYCVcdTAwMTNlbr/uPt6b/U+nlXpNbL+xXHLpXCJJ9zbo8Pek0X49f1/HXCJCXHUwMDFikFGsblx1MDAxOcxg5Pz8VTRcdTAwMTSwl+GZhOBLwkNcdTAwMDCrPVx1MDAxMY1naDtsIVx1MDAxMFx1MDAwM/6qNJZxTKRcdTAwMTbMub+9P6jIkthcdTAwMTD7N7vV0+yXyu7Jp+5b6sHjJEnLmVx1MDAwZjsqaTn8ht1hX75cdTAwMWIlvHNPWpJJ5mVcdTAwMDJcdTAwMTkt0NnxbUz6Y17SrCWbj1S5VTogXCIgwYrMoJnjYlx1MDAxOVx1MDAxYSm38VYlL4LK8bFmV1xyXHUwMDE3767Hqkhv5PLLQFx1MDAxYcdxXHUwMDExsNUkNp+q56o0Mn9cdTAwMTh/zHNm84+wRlx1MDAwM2z+cIpcXKWRid1cdTAwMTaMXHUwMDA1/uBcdTAwMDTiWL86XHUwMDE3XHUwMDBlz8zuZeuomTvcKOWz0cZyu3ygfdHU8lx1MDAxN5IgXHUwMDA0qD5Z9JlcdTAwMThAJdnP8vHJVFx1MDAxZV9cbptcdTAwMWZcYtHPQrGDXHUwMDE505DxfI/hXHUwMDBiNMuqRf5ucNFcdTAwMWFcYt/01yyc6eCthptALlOhmsjmd4lcdTAwMGLAQLKDXHUwMDAziGp8mJrjzY2mOP1aKT2stHdcdTAwMTGuXHUwMDFmSup+6Vm/nZ1iiP/zvTbQ8S/0dlx1MDAwNnHCXHUwMDA1gFKC8ett3Vx1MDAxYzeUmFxym99cdTAwMGbgJP5cIv4uXHUwMDA38fdcdTAwMDex+V1ipYyjXG5jhJ2gJ8Xh5Vx1MDAxNT5cItjbp88n7UyYscfl+vrSyzVh4Fh/WVx1MDAwZehZtEH0eoNeqn3mRYBcdTAwMDZcdTAwMTNfXHUwMDA3O3suv7K+14+z7Fx1MDAxNzlcdTAwMTVPxCWS+c1ghVtpYJy8qS/FL6nunJsxnT/prXbOXHUwMDBlvM9cdMU6vVxmTsmdvKTfzcw5sFx1MDAxM3ScuXiWpcszudW42C192s/WcpWd99G7Vvt9kzjqY1x1MDAwZlxydN/CbemMX4PnwDdQ0qzyZuBhTkHLXHUwMDAz4djnXHUwMDA1WjAtb1/kLteal+p0o3L7eFx1MDAxODZcdTAwMGJPutTtstxcdTAwMDO5SbIwv4b9NeyoxNnwXHUwMDFidof9rlx1MDAwN2dofFJValwi209TYubMt4OUksZffJD+lJdWl7IvlKZLwVx1MDAxN8VnpUtnwvdcdTAwMTNcdTAwMWOzcyxml7xLV/fFv0++31xi12BcdTAwMDF8v+RilFG+i9dcdTAwMDR+XHUwMDBlbd5dXHUwMDE3zvbWqfglX4n29s/3dlx1MDAxM1uWLlx1MDAxN99cdTAwMGZE4FxmXGKjhe9MKmNdsTqRjPeE/Fx1MDAxZbdW+35mMD82rla+RYe0VnC8KmxM+mJ0P1xiXHUwMDFjz5M0XHREgYP1KIlcbv062Fx1MDAwNYuu8f06ZmVL0iuoyV3LJOtQ313WjV9B3dws2Mdy9sneX8rdtdXSefnpxC053Vx1MDAwZnyvZd+e1oKxMr4v6je8YmB8/zZcdTAwMGXBnW9cZjlHvFJcdTAwMDDI01CgXHUwMDFjuWHL6MFcdTAwMDZcdTAwMTZcdTAwMDU4w+FcdTAwMWNcdTAwMDdxclx1MDAxMK+ao1xiJFx0i6amWICZ4TWNmpKywMuT01x1MDAxY6hcdJrsrWx9Pmt8LbSL5nzncdvIjcuzXFz0XHUwMDE2tC6Qm4K+0Vwi61dcdTAwMDPot1x1MDAxMOjrXsp/f1x1MDAwMM5cbuf33o5b/1k32mV/XHUwMDA2pee/alx1MDAxYq9cdTAwMDHGqSmkpG88pFl76njm/rUntFwiYlxys3By6qKgSoksKraBrN7dXHUwMDA0u9qf4Vnt071dWa3uX1x1MDAxZdRcdTAwMWWbd4XjZpKnvixI1a7TXHUwMDE23PBLVtag6stoXHUwMDAyXHUwMDA2orPNXHUwMDE2P1x0RtN0O/KltdmdXHUwMDAxVFx02Vwiszgtmke1IKhKkbJTu2CsXHUwMDAyXHUwMDFim/FcdTAwMGJra7dnj2H2aKNw0FxcvTtcbqN2e+UmKVx1MDAwMb8sYLXsK7K2s8Yho9U521x1MDAwN1ZcYpRU7Eii9WCdikSV2md3XHUwMDA2YDVglVx1MDAxNvGC/ftcdTAwMDNrusuarFn5PVx1MDAwMitcdTAwMWRN41x1MDAwN1rR7v6FuSiXTu7yrvX5oZy5XFw3XHUwMDA3S0/665CHtGVb70j5/d178CqFsYHjqMY3aFx1MDAxMHK69oMjSH8qkMR4XHUwMDA08MyJYYtUtPRcdTAwMGK8wCm/XHUwMDE3sVSDi1RYu0jPXHUwMDFmgDm0hn45MTTJuHef39v7slx1MDAwZke3mYeHwu1D+2v1tjF97vK9XGY7KiU6/IbdYV++XHUwMDFipVx1MDAxM3BWOiFxa3KV2JBcdTAwMDVcdTAwMTjbXHUwMDFjSdP4XHUwMDBiK9Kf8pKmREHadG1gtd9Lxlx1MDAwN4ZcdTAwMWHQzrNcdTAwMTVcdTAwMWONVFx1MDAwN8O4hFx1MDAxY15r86adXHUwMDEyp7NYXHUwMDE4S5a/uS+wY1fW51x1MDAwZXhAXHUwMDE3V7UjuYSPXHUwMDBiJFx1MDAxM46wcoNkwsdp2IRp7F70q7xgXHUwMDAyPkflrPJ8eYqVK5cp5FpHn3dL9XJ5ud1JXHUwMDA2XHUwMDAzXHUwMDA3N563Yp3xLYJVv0D6XHUwMDEyhjWecKuJY/qp/MlcdTAwMTRCIYuWIa2VUsbK2HL0eEZJd1x1MDAxYTmBYdUh41x1MDAxYkl9zyhJQyB/SOwzs/XOibyj5Iy95qjUM9HHhmmzoj+3r0ru6GrroJJTh3hw/by29LSjXGaA8cuZlFx1MDAxM35xiO03XHUwMDFk6L03v+LL75dn58c7mlx0mZA63YTe5EdOyzqahPtcdTAwMWGbx0/NOvoxXFxCflx1MDAxZonuIN/MeUU3vlRHVysnbnPzZrN+9OX86Gz1dOP8aHXppdrIQHg2l187KGz/JvZcdTAwMWShdlxuXHRcdTAwMWRcdTAwMTA//DluxFwi/G5cdTAwMTVcdTAwMWOFalwi8LtFqGE2XGI5WGXH1SpU7Fx1MDAxYmhcdTAwMWOQa09qd7pni/Nfgv1cdTAwMDNcdTAwMDU7k/Ja/WfghU4o2iPK7GmbLfL9/Fx1MDAxMo2x5Xun0ag8rjdk5n5/7aZyer5xXHUwMDFh3W++jzK7XHRYvK1Fllx1MDAwZYfxXHUwMDFkKF9k3O/J6Jv+k1x1MDAxMobN99xk3PcpXHUwMDAxLYxfy2ZcdTAwMDTqYYxhwsDybKzwsZ52XHUwMDAzIaBf1sUgolx1MDAwNW9cdTAwMGVmJbpJXCLAt6PWqcSQXGL4Vfq93cavsl9X7E21JU736qtHxbtnVXu8v3xTgn2xcFx1MDAwNe3/Ut3pKY5cdTAwMDMmSTFcXK3SSrJTXHUwMDAwen6kLU9cdTAwMGLhgFx1MDAwNpTPXHUwMDEzW6GG0EJ8W1x1MDAxZOFcdTAwMTkqwneCwngg8lx1MDAxYVx1MDAxNfl9j1x1MDAwNIpcdTAwMDWnLCa1SKmATeus41x1MDAxMuvsfomCJ++MX1x1MDAwZrpoP118udtsVWBlz62V98rm4K603Fx1MDAwMTzrhYBcdTAwMTHg9SqrWCn7o1wi0lx1MDAwMVjR2fC4t0nYjNvqsL8qXHUwMDFjKE/NxyHrXHUwMDAxfTXIoNDaoensXHQ2WFxyQmfYu47npX4qoDqdXFxcdTAwMGLyPVx1MDAxNk18O4GRXHUwMDFkoGzt881u5ejE1Y+b+TD6VMmJJc808fNcdTAwMGV8Po3YzKPyPn0/Ulx1MDAxNYdcdTAwMDLWOWPQTFu4TGurMz1SwTdcdTAwMDNmQ/iTXCJVxnfY6ocqR8BW9qzaXHUwMDFkzbTbyVx1MDAxZEa3ollcXL9ccq9BrV/Jh+aSY9WpQGnyvo6RxJI7oFUx4JDUNyxjZ9HAdGo1rbHO1GBcdTAwMDVgpLr4XHUwMDFlQ+9cdTAwMGarI2ihKVxcZn5cIuArumOD9TjKStrP5chd01x1MDAxOYX526eVqkhcdTAwMDDrXHUwMDEy1dhd4DpcdTAwMTU1wSGnjL3rb5vag1xiUDFI+Jx05KZSrSNq7DqQ7GyA51OzN+KGRFhaXHUwMDA27KZ4rq5veyjt4EJr3+uCT8/DY305MbRsfV3BXHUwMDFjfcpcdTAwMWTa7fVjzK5uhZtccnszfTX8v3zYUbX74TfsXHUwMDBl+/LdwnRNYlx1MDAxZiCZzOf1IZiRQo+vZtJcdTAwMWbzslx1MDAxNu9BpatcdTAwMTlcdTAwMGWcXHUwMDE1XHUwMDA3rEJcdTAwMTjfXHUwMDFicY5qhkarmSHFe3ROk1Y/YvvQN+Vme1x1MDAxYlx1MDAwMVx0tvNGKfZcdTAwMDaI/1xi6rkqvXhcdTAwMWbFn/Scq/cj7OeQ6n00RfneQvJcdTAwMGVHXHUwMDE2pfKZj/EzrNnzfFttnn2l/f1wd0OYnZvPoV1uR1x1MDAxNYVcbsjvU+1cdTAwMTe/KG1s31xuQ41s+i1pY3zLrFx1MDAxZVx1MDAwZeaM+1x1MDAwMVx0XHUwMDAyo3zlnuLNe+OJKl+0l4o/woe6XHUwMDAzjqpB1Fx1MDAxNlx1MDAxMN5FUPXbi0X6mG82j1wiXHUwMDFlkk9/gy7Pulp8kdruMFx1MDAxZu+r4cPqsEC18/FcItBcdTAwMTFcdTAwMDBcdTAwMGay0M/5r79/+/s/t9XlXHUwMDAwIn0= events.Key(key=\"e\")events.Key(key=\"x\")events.Key(key=\"t\")Tevents.Key(key=\"x\")events.Key(key=\"t\")Teevents.Key(key=\"t\")TexText"},{"location":"guide/events/#default-behaviors","title":"Default behaviors","text":"

    You may be familiar with Python's super function to call a function defined in a base class. You will not have to use this in event handlers as Textual will automatically call handler methods defined in a widget's base class(es).

    For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Left and Right keys), then Static.on_key, and finally Widget.on_key.

    "},{"location":"guide/events/#preventing-default-behaviors","title":"Preventing default behaviors","text":"

    If you don't want this behavior you can call prevent_default() on the event object. This tells Textual not to call any more handlers on base classes.

    Warning

    You won't need prevent_default very often. Be sure to know what your base classes do before calling it, or you risk disabling some core features builtin to Textual.

    "},{"location":"guide/events/#bubbling","title":"Bubbling","text":"

    Messages have a bubble attribute. If this is set to True then events will be sent to a widget's parent after processing. Input events typically bubble so that a widget will have the opportunity to respond to input events if they aren't handled by their children.

    The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the \"No\" button focused, it will receive the key event first.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbVPa2lx1MDAxNv7ur3C4X3pnarrfX85M54yioFSxVk+tPZ5xYlx1MDAxMiElJDRcdCDt9L/fXHUwMDE1UFx1MDAxMt5cdTAwMDIqcHBu80Eh2eysvfZ6nv2slZ2fW9vbhbjXclxuf2xcdTAwMTece8v0XFw7NLuFt8n5jlx1MDAxM0Zu4MMl0v9cdTAwMWVcdTAwMDXt0Oq3rMdxK/rj3bumXHUwMDE5Npy45ZmWY3TcqG16Udy23cCwguY7N3aa0Z/J36rZdN63gqZcdTAwMWSHRnqTXHUwMDFkx3bjIFx1MDAxY9zL8Zym48dcdTAwMTH0/jd8397+2f+bsc52zWbg2/3m/Vx1MDAwYql5nODxs9XA75uKOUKcMCT1sIVcdTAwMWLtw91ix4bLd2Cxk15JTlx1MDAxNXrRt6vuXHUwMDE34pu8yI/l/snt58reSXrbO9fzzuOeN3CEadXboZNejeIwaDiXrlx1MDAxZNeTu4+dXHUwMDFm/i5cbsBcdTAwMDfpr8KgXav7Tlx1MDAxNI38JmiZllx1MDAxYveSc1xiXHLPmn6t30d65j6ZIcVcciQ0JZprLDVVw6vJ77UhMeJcdTAwMTQrjSTmmo3bVVxmPJhcYrDrP6h/pJbdmlajXHUwMDA25vl22kZcdTAwMTFL48yYu4+jVdRgjFx1MDAxMlwiwVxmqlx1MDAxMeHDJnXHrdXjpFxyIYZCTCjJXHUwMDA3t8qY4vSnhGtFqVx1MDAxNul8JbdvXHUwMDFk2f3Q+GfcoXUzbD04rlx1MDAxMCVfMqYnVlx1MDAxZozHVTa2MpNOxFWl7Fx1MDAxZlQ+fyzvX1rfj3ul8/tw2NdIIMbOfVxcXHUwMDE4Xvj19ne3M7tcdTAwMWRp/XbRXHUwMDFiLmituL+6b1x1MDAxZvPDkPpnJdwrnXlfet3p1pphXHUwMDE4dDP9PnxKo6ndss1cdTAwMDEjYCEoSyiDS8SG1z3Xb8BFv+156bnAaqQkspUxeIK7RsafIS5G0fjZR+JcIohSXHUwMDA0WOApiOZcdTAwMTFX/vRtKnFplENcXFx1MDAwMlx1MDAxOVx1MDAwMlx1MDAxMyBcdTAwMGLBXHUwMDA2zPVcdTAwMTLiikPTj1pmXGJ8MIW85HzyXCJcdTAwMTNkRVx1MDAxMJFMJ3S2fLrKj05O5Vx1MDAxM6IzXHKCwI/P3Vx1MDAxZv2lUVx1MDAxOFx1MDAxYyuGiIBBaI24XHUwMDFjaVUym67XXHUwMDFimdd+XHUwMDE4g+W7rdab/2ZdXHUwMDFkOWDCYLlcdTAwMWRpvOu5tSTOXHUwMDBiXHUwMDE2XGbKXHRHIFx1MDAxMLsgXHUwMDA0hlxymq5te5lwtMBcdTAwMDJcdTAwMTP6XGaPXHUwMDE2WZOD0K25vuldjFx1MDAxOJhcdTAwMGLJXHUwMDAxJUzBpFwiszGJwfGYscyiNVx1MDAwZpP5JLWhmKRSXHUwMDFhhDIsZKJcdTAwMTVENjL6XHUwMDFkMGJwwCsgUilBJScrQyU1XHUwMDE0cCSTQlxugShXelxuKqk2XHUwMDE0xVopLjSGcJ5cdTAwMDAp5lxcwigofjpG+6Y+XHUwMDFio09bQTJ2mGG85/q269fgYrr0ParkRTDRR7HVTqzcXHUwMDAxhlx1MDAwNYBzjlxiU8BcdTAwMWNEZFx1MDAxYdXMVlx1MDAxMvRcdTAwMDZcdTAwMTeKSFx1MDAwNbHNlCZYPDRcdTAwMTiuwFx1MDAwNce355tUvPlcdTAwMDbS8NI8s8t2T/rt3a87++VZJjGsMdZIYyqJXHUwMDEyRLFcdKswg+mHmcNEXHUwMDEzglx1MDAwNPydMMszo7hcdTAwMTg0m25cZs7/XHUwMDE4uH487uS+N3dcdTAwMTO011x1MDAxZNNcdTAwMWW/XG7Dyl5cdTAwMWKnhVbS46h4TD9tp7Dpf1x1MDAxOX7+5+3U1juzozk5JuI47W8r+39cdTAwMTajhY5cdTAwMTVcdTAwMGbwPIXVXGK4eFx1MDAxNq1cdTAwMDGSXHRcdTAwMDaxsbjSyJ/nXHJlNaKUQSVcdTAwMTJIJ1x1MDAwYp5iZIzVNJBcdTAwMWWiSFx1MDAwM2q1TtbF1WlccpHOxZDGVKp8XHUwMDFleFx1MDAwYphVipHVZi2pkGp8iM5kpV5FOLBkLL+dWl9PXp5cXHjNXHUwMDFmn2/+wlx1MDAxZr2DYlxye3Xkur1ytJhcXM/t97LyudM9ZvsnxyG3yz3SJmxfLKFfcmlcdTAwMWZcdTAwMWSWXHUwMDFh1onaZfii6Z1cdTAwMWX4X2tL6HdF7n1d3TbE5VGn1PqEb9pRvdHhJXRXtf/vnPuCXGZ2vebOy+On33BBa0vlxoUon3+7u6yfdSp+3Tv+XHUwMDE27CzBXHUwMDBi91fki/zavjmpoHL5XHUwMDE2k2bN6Z0tqT7AJFx1MDAxMZCOrro+QIhW46dcdTAwMWZXbYq51ELRxXOR/LDY1FVb07xVXHUwMDFiUjJDrmnV5lNW7Uxq9LBqKyk4XHUwMDE4K+g6K1x1MDAwMkxohTR6QjxOr1xiLFpcdTAwMDEoPqbnb6795IJrv79OKvReULsuXFz704tcdTAwMDOZPHGkOOA5d6PR/6TSwFx1MDAxYy06Xlx1MDAxYZhr+fM1NkWKzkIrRlxcI0GzYTFcdTAwMGaunVx1MDAxYoJlLf7c2bPo0W3ZP2qflXv/Llxc+Ty0YkFcZqIxpDmSUsGoXHUwMDFhRSucMrhmSlx1MDAwMVx1MDAxMFx1MDAwNWFMrlx1MDAwZaxcdTAwMTkwpFx1MDAxMluMg5VgjDBkY5lq31o09sVheFOMnfaN3K1Wrq7qny5+XHUwMDFjfvitsZelsVfk3tfV7ao09uvywqo09uvygqf3iuq2XFxkRSGc84tcdTAwMGb3tVJpg72w/JRgXlx1MDAwNjN9IGm3XHUwMDBmn3JcdTAwMTSYwpqwpyiwXFyhMSsjoCSjcMc1XHUwMDA2wVRRgejiXHUwMDFhI3/+NlVjyHyNodelMdRcdTAwMTSNISc0htKYScroXG52NMxcdTAwMGJH/IRwfFlCsNeO48B/k5xcdTAwMWLo6uvClVx1MDAxM11cdTAwMTfeXHUwMDBlvnXM0DX9XHUwMDE4pHbUtixcdTAwMTjd7CxBjna+pCxhjphcdTAwMWXPXHUwMDEynjecXFxE56dcdTAwMGV49lNHjFx1MDAwNYQ8hPvimf7ljYjrxdZt1W+ffqrY9ztcdTAwMDeXZ/VNz/SpVlx1MDAwNoaoXHUwMDE1QkHSz4SczFx1MDAxZJCAbjjTjJOV7lx1MDAwNVgsecCISkQ5ZWtOXHUwMDFl0MfK4Un7qnL48axcdTAwMTL39lx1MDAwZXo8OPF/J1x1MDAwZstKXHUwMDFlVuTe19XtqpKH1+WFVSVcdTAwMGavy1x1MDAwYqtKXHUwMDFlXpdcdTAwMTde8DjhmTnJ9IGk3T58yntKwUEkp09cdTAwMTBWlpPgmTlcdFGCXHUwMDEzQcjie1x1MDAwYvKnb0O1XHUwMDBiQzRfu+i1aZdcdTAwMDWTXHUwMDEypSmiXHUwMDAyr0C6LDNcdTAwMWWXnpRUgylcIt5cdTAwMDG8huvOSOZo9Fx1MDAwNTKSeWPJXHUwMDA188z9j1jJmc9cdTAwMWMx0kJLxjPhPVx1MDAwZs75tLmhcKaKXHUwMDE5QiCtklx1MDAwMoLi2YpK/6Gjklx1MDAwNlx1MDAxNZJizYHeOFErrDFgXGaWXGJGXHUwMDE4p5RQwtkkulx1MDAwNTe45lopRGjyglx1MDAwN1x1MDAxZFx1MDAwNztnyXNi8ZyNRM/fXHUwMDAw+Vx1MDAwMrAvuFx1MDAwMXLh3YbIYJgqrFx1MDAwNeVIJvOVaTPYaUhcZlxmPoZlilxuLlx1MDAxNOTakztcclx1MDAxN9pcdTAwMDCZXHUwMDBm6lx1MDAxMZO4XHUwMDEwyVx1MDAwZUBcdTAwMDSrXHUwMDA1pJFcbk/YXHUwMDA0M4+0XHUwMDA0vIE9SMBcZuNcdJte0+7H2ZGcXHUwMDFjXHUwMDEzMZx2t5X9/1xmOpstTmBcdTAwMDKoktn8fVx1MDAxZZvlXHUwMDE3pjeVzSQxINDAXHUwMDEzXGYpxHXqj1x1MDAwMZkpQzCMOaeKXHUwMDBi/qJXw+ZQXHUwMDE5NVx1MDAxOMijxFx1MDAwNM4kKKUpVEZcZlx1MDAwMcJcdTAwMDR0iVJcdTAwMTIxltnv/Vh0oUpcYs1AvKyXzJgmXHUwMDE5sfQvktlcdTAwMGVQXHUwMDA3lVx1MDAwMCTMQEpKJsgkdYCjKbhZUVxuLCM0aM7n0Vl+1XTMqMQmXHUwMDA07UEkSEz0hFEw/UQgcCeiXHUwMDE45CdWr5rOdmaHc3JMXHUwMDA28lx1MDAxM1x0LbdcXCwzr7OOcVx1MDAxYeeYwaLCXHUwMDE3L1x1MDAxNvNcdTAwMWatfetL7XDvwlx1MDAxM7FC3S46vmttOqdcdEZcckWJgHDjXHUwMDEyKzZZK9ZUKFhQYGXNvlT2nPddmSWcO87YJKVcdTAwMTGR9pxcdTAwMTaKM1vQXHUwMDFlOEtCqqVhTp7xXGJoXHUwMDFlZ1xyw2pKyaLt69L9j6/V71cq+u53j3ixcVJ9eSWkKE87ptM+/G7+1Y0vT2k1qsYzXHUwMDFl+i6pXHUwMDEyMn0gabePiJrN36BcdTAwMDKUeMrjsFxccM6qhEiSkzrBIVx1MDAwNVx1MDAxNotcdTAwMDMzf/o2XHUwMDE2mCpcdTAwMGaYmsHStCRg5qpccsKnQJNMpEZcdTAwMThcdTAwMDNXcs7Uup/OPjFcdTAwMWPTWU9cdTAwMGIhmUeGI4WQdJCPhVx1MDAxMKeT2GR8cHpvXHUwMDFhTu/9deHiujDjXHUwMDA1Tj3y46W9wDlnkVx1MDAxOa92TDd4gMmtXHUwMDA3oFx1MDAxN8xW6zxcdTAwMDa/XHUwMDBlXHUwMDA1XGZMnWs/OCf1ZaHjOt29Kbx+1z+SXvs4T1x1MDAwMOUkXHUwMDEz9/PX1q//XHUwMDAxXHUwMDA3vCMgIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")

    After Textual calls Button.on_key the event bubbles to the button's parent and will call Container.on_key (if it exists).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXG1T4spcdTAwMTL+7q+wOF/2Vq3ZeX85VVu3XHUwMDE01PXdVVddr6e2YohcdTAwMTBcdFx0Jlx1MDAwMcSt/e+ngyxJeFx0iODi3U1ZXHUwMDAymaHT09P9zNOdXHTfV1ZXXHUwMDBiUadhXHUwMDE3/l4t2Fx1MDAwZpbpOuXAbFx1MDAxN97H51t2XHUwMDEwOr5cdTAwMDdNpPs59JuB1e1ZjaJG+PeHXHUwMDBmdTOo2VHDNS3baDlh03TDqFl2fMPy61x1MDAxZpzIrof/jf9cdTAwMWaadftjw6+Xo8BILrJml53IXHUwMDBmnq5lu3bd9qJcdTAwMTCk/1x1MDAwZj6vrn7v/k9pV3bMuu+Vu927XHKJekKiwbOHvtdVVWpMJNOc9js4YVx0Llx1MDAxNtllaL1cdTAwMDWF7aQlPlVwS+dccr0vNre3qvdRdHrepHf1teSqt47rnkZcdTAwMWT3yVx1MDAwZaZVbVx1MDAwNnbSXHUwMDFhRoFfsy+cclSFdjxwvv+90Fx1MDAwN1x1MDAxMyTfXG78ZqXq2WGY+Y7fMC0n6sTnUDI+06t0ZSRnXHUwMDFl4lx1MDAxZVx1MDAxOFx1MDAxYlgozjiTQlLBkvF2es1cXCiKNSWSQC86oFnRd2EmQLO/UPdIdLsxrVpcdTAwMDVcdTAwMTT0yklcdTAwMWZFLI1To27/XHUwMDFjr6JcdTAwMDZjlFx1MDAxMEmJplx1MDAxYVx1MDAxMd7vUrWdSjWK+1x1MDAxMGIoxISS/OlSKSPZ3UlRQlDCiEh0jK/f2Cl3neOfQZtWzaDRs10hjD+kdI/V3lx1MDAxY/SstHel5t1sX3ok2tza31x1MDAxMZfhiSutVl3gvqyMK0b2Q1ToN/x4nyf24XBcdTAwMTe7Zpl9uSi192osujQv9uhosWZcdTAwMTD47WnlLkjd31xcbKb3+2kvmIjtvUuctNkom09Yg8GtXHUwMDE54kRwOPrtruPVoNFrum5yzrdqXHQ8raT0XHUwMDFkXHUwMDAyxYyeKUTkaixcIjKlkYbwTHSYhIj5Vl5aRFx1MDAxNLmIKIjBJII5QfLliFx1MDAxOFx1MDAwNaZcdTAwMTc2zFx1MDAwMHBmXHUwMDA0KsrJqEiGUJAqzVxilkLPXHUwMDFmXHUwMDA15+mdiVx1MDAxN/hedOo82l1ZXHUwMDA2x4ohXCJcdTAwMTCRWiMuM722zLrjdjJcdTAwMTPbdWPQfL3RePeftKVDXHUwMDFiVOjK5JnO665Tif28YMGg7CBcdTAwMTNcdTAwMDKRXHUwMDAzXGaj36HulMtuylx1MDAxZi3QwFx1MDAwNJnBzjSrvVx1MDAxZjhcdTAwMTXHM92zjIK5IfmE4iNiUlx1MDAwYjYuJqmG6eaYTc9S8peVJY1JgpBBXHUwMDE0xKRElFx0hjHLxCSh2kCEMM41XHUwMDA355dcXCwsJpGhpeSKgzaMyVx1MDAxMVx1MDAwMcm4QVx1MDAxNVx1MDAxMlxuaU0wk1SKwVx1MDAwMMVUUIlcdTAwMTVHM/CUrqazRiggXGKZJULDyFxmolxyxys7Xlx1MDAwNVx1MDAxYZN17yf5niZcIroxbDXDrlxyXHUwMDExo1xu0JMpXHUwMDAwUoKFZDzVrWI24oXIoFx1MDAxOMGUY1x1MDAwNeingIj3OvRcdTAwMTfggu2VJyvFOo9cdTAwMWN92l5v75OrWqd0XCK+XFw0ySil1kArXHJTg5HATFxizaRcdTAwMWVWXG5TQ1x1MDAxMVx1MDAwNViHOcFYa4yHtHLNMCr69bpcdTAwMTOB9Y99x4tcdTAwMDat3DXnelx1MDAxY+xV2yxcdTAwMGa2wqjSbYOo0IglZilp8m41XHSb7of++3/ej+492pnjY9iNXHUwMDEzYSvp13FoXHUwMDE22Fb0XHUwMDE0zCNcdTAwMTCNUDpcdTAwMTbSMIQvl1qpXHUwMDA0KiZhWv4kLymmQXppYIUhX0GUUKF1lmdcdTAwMTCtXGZCMSyGXHUwMDE4QStTfECzOfJcZpFgVFx1MDAxZseUXHUwMDFhwi2CtITVJoV6r5Jfbd7W6N3mbadxb53csk7rlG6ffVre/Opi97zV3melg/2Al7c7pElYScxBLrko73zaqllcdTAwMDdqneGzunu06V1V5iB3QeZ9W2Jr4mKntdU4wd+aYbXW4lvo9rD8x7hLLfaRblx1MDAxZu/fsiNyXiu5XHUwMDFi9p44q1x1MDAxZOzOo0Bycllj1YOz/a/0+HHnq/ulxYLSi+ROKlx1MDAwZYw2UFwi9ueCm0PuuEB44cVcdTAwMDFC2fhlWyhcdTAwMTmTXGKS5J2Tlu18v1jWZZuSvGWbXHUwMDAyRZSvtGzzXHUwMDExy3YqZe4v20RLyqRIxvtcblx1MDAwNVx1MDAwMaZcdTAwMTTCXHUwMDFhP8MjR1x1MDAxN1x1MDAwNKYtXHUwMDAwXHUwMDE0f2bn7669uMEpf7yOK/+uX7kuXFx7o2tcdTAwMDOcZOT0U3/Xvs36/7MqXHUwMDAzXHUwMDEz2OhgZWCi5rPTbHDG8fc3XGLTXGLy0OnD9ZN9ZNPHc+fh6/bxSXX7xrNcdTAwMWXto19cdTAwMWKufGK0XHUwMDFhXHUwMDE4XHUwMDExXHUwMDAx2CgohFx1MDAwMYCTyEQrXHUwMDEz3Fx1MDAxMMCyIVx1MDAxOedap1x1MDAwYlhzXHUwMDBmVo2Gg1VcctdcdTAwMDY0xZhS9dr3ME7XnMvS/tq3b0FJ31xir3VPXHUwMDAzbP3h2PPi2Fx1MDAwYjLv21x1MDAxMrsojv22rOBeXHUwMDE2vfXO1n2FXHUwMDE3afXIosXjqIh+PyvojaK62S6yolx1MDAxMPbp2d5DZWurvbxWWFSqMXd1J2VcdTAwMWGjL5iI7b2bJ6/LpS/jMlxySphcdTAwMWE8nTBcdTAwMTeluKY0YbqTmEu+mZeTuYgsc8lmXHUwMDE5TKLX4i1qXHUwMDA0b1x1MDAxOXFPQ1NcdTAwMWRXilOT8n+YZGw0o8j33sXnnrj6deGrXHUwMDFkXlx1MDAxN94/fWqZgWN6XHUwMDEx0PewaVkwuvGZh8xcbp9T5jGBoVx1MDAwZmZcdTAwMWWzXHInN54npCNCjlx1MDAwYmrOXHUwMDE44/g5NzLXLiufxVWntLPjXHUwMDFjuvbnzm7n7mHpq1x1MDAwN4RcbkNThFx1MDAxNCVcXGlAuYG4hnxcdTAwMDTBeYw4U9BXysVcdTAwMDX2dFx0XHSDMVx0qVx1MDAxN1A7yKWK+uiIq8+HzZ3L5teaf2Fe3W3JP+nIvNKRXHUwMDA1mfeNiV1QOvK2rLCodOSNWWFB6cjbssL8b3wsSN1JWc7oXHUwMDBiJmJ775Ygy+Fjs1x1MDAxY6KB4Ovn3E7JN/OyXHUwMDEyXCKGc1x0XHUwMDExJDqvRYimzHRcdTAwMDRcdTAwMTJUKM5cdTAwMTdQoV3qTOfQXHUwMDFmkVx1MDAxOdiAXHUwMDAzwWunOVx1MDAxM5j/XHUwMDE0ac6kseRG89h9mlx1MDAwNOHxt0fBwbHkWj5j93QuXHUwMDFjL2s8XHUwMDEzajBcdLFEhGSMZm+3UKVcZsA0QZiIN/nSXHUwMDA1XHUwMDA2M8aGXHUwMDEwglx1MDAxMcYpJYAtbDi2IdeK94tCZFx1MDAwMdBcIonpUKgrgCNGOZmhqDH7Rs1cdTAwMTeE+pRcdTAwMWI1p95cdTAwMTOJXGaGYfhgXHUwMDAyKjhHhKhUp6dcdTAwMWSRxMBgZULiXHUwMDFlQilGSa/HM/dp5sd0RicuXHUwMDA0LFx1MDAxNTzeXHRMJUpcdTAwMTC6r1x1MDAxM8w90lJoXHT6IFx1MDAwMXP8tndpjvfl+Fx1MDAxOPLiRNxK+vXZaIY104Onkz2aSkpYtNn0ezTzS+jLWYMlYHkpkFx1MDAwNk+iUjCW2lx1MDAxM/lcdTAwMDRn2oh3XG5cdTAwMGLEqKZcdTAwMDIrNqDYPPFcZmBVXHUwMDExrVx1MDAxMEw2k0IlO1x1MDAxN1x1MDAxMjwjhlx1MDAwMColMFdKXCLGdKoq3Fx1MDAwMzREXHUwMDAwk1x1MDAxNVEzlHNmXHUwMDA3NPjHuUx86Vx1MDAxN1x1MDAwMtpcdTAwMWGgXHUwMDA3zKRgmDGAdSZIqtNcdTAwMTN4gJ0pWFlRXG44I7RmM248z6/FXHUwMDBl6Fx1MDAxNKuEgFx1MDAxZmDALZxQ/tXUvnNcIrrPXHUwMDFjUayUxupNXHUwMDAz2tp4b46PYT9+JqTlXHUwMDE2oWFix6NcdTAwMWFnXHUwMDA0ol1PX4Qu31x1MDAxY1/c3947l6XPx7e6yuT5oW3+WlSjk1BccixvSLA6XHUwMDAx6ytcdTAwMDbeNLwnRlx1MDAwYlx1MDAwZcu6XHUwMDEyWCP2omdp/mKWsG85Y8OQRkSCpklcdTAwMDE6wbVcdTAwMWVmcZhcdTAwMTNccvrMsOl8XHUwMDEyZPXdakTNorTl7IR8XHUwMDE37Z5cdTAwMWNcdTAwMWY+3lhHlebGWvHlJZaiPGqZdvPTvfmlXHUwMDFkXVx1MDAxY9HD8DDam0OJZe7qTiqxjL7glNo6n3evLm4qdrBn1qnliK90s/FtOiv8XHUwMDA0gDz6XGZ/iWstqHQj5djKXHJGgoLLajZ9qpc/fctcbiMyXHUwMDE3RjQ3WFx1MDAxYUZcdTAwMTaX7KWraMmDscPpXHUwMDFjQLtcdTAwMDI6J39B5Wam5+5SlVx1MDAxYoIyZ/uVm2QoPys3divWydizO+9qdufjdeHsujDmyVid+fLcnoydsCZcdTAwMGWWZ0YrPPtcdTAwMDKvUyvWYE1cdTAwMTWmgrDnZC3ty9ZR5+BTZJ5vr31ztrfq7EBtLntcclx1MDAwNlJcdTAwMTFDQqIo4u0jXFxcdTAwMTA+UIVBkDJcbowhhWNAhNGLXHUwMDAy8+VcdTAwMGI8dIOo1LM8r/6SXHUwMDA13lx1MDAxNlx1MDAxYid3ev2uuVtad+W3dus4OKsv71x1MDAwMr8gdecudlx1MDAxMm9cdTAwMTh9wd+HNyg9dkc+iZ+Fxlx1MDAxMLfT3/LJn76lhSeRXHUwMDBiT5RcdTAwMWJoXvA0XHUwMDE33iBjXHUwMDEywyVdwHOvf3hDwlx1MDAxYiastXPgXHJja52pZ6NcdTAwMDb3pXFcdTAwMDSrXHUwMDEzXHUwMDE208dkPkg9KybJq8UkV8JAkGjHv66VpfFcdTAwMWNcdTAwMTkqrktROX6TqS3iavDsgSjhXHUwMDFhXHUwMDA0UTzqXHUwMDA3brgwQC/IXHUwMDFmXHUwMDA0RfG2XzlcXNVkQmtMkX7d2zRwSZz6XHUwMDE1g/lXNfN59Gr6llxiXHUwMDAxnGRIi/hX0SBcdTAwMTVcdTAwMWLxc1x1MDAxYVx1MDAxOEBVXHUwMDAzmGI4YmuyXodnVjXzY3RAJ+B1Or6nT1x1MDAwNJNsSCNhXHUwMDAwwnVnlOk4W6ZDXHUwMDFhvama5rBcdTAwMGZ3T1x1MDAwZrtvXCJpJf36bFwiMb6MyblShKR/X2ZcdTAwMTJmSeteXHUwMDFkXFzV/eKRf3/X1vqiendcdTAwMTUuP2ZJXHUwMDAz+Fx1MDAwM9aKgWdjNlx1MDAwMFxcSFx1MDAxYZpJQYVWWDGxuC3yPFlcdTAwMWJcdTAwMTJcdTAwMTYxjFI0vlx1MDAxZFx0gfeqv8slXHUwMDA00UrNhFJTsIjhfSM3zZub9Fx1MDAxMp+mXHIq03vavSCR31x1MDAxOMdcdTAwMTgyo1x1MDAxOKRcdTAwMDc9TZ5ia6VcdTAwMTe3XHUwMDA1s9E4jcBCfYiDSXDKvWEm8lxuLcdub4zIdm+7Ryy1XHUwMDFir3Fk2PFcdTAwMTR8/7Hy419iJnwqIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

    As before, the event bubbles to its parent (the App class).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT4lhcdTAwMTP+7q+w2C/zVlxy2XO/bNXWW4p3XHUwMDFkXHUwMDFkXHUwMDA1dZzXLStAhFxmgWBcdTAwMTK8zNb+97dcdTAwMGYqXHS3XHUwMDEwlTgwO1QpkFx1MDAxYzqdPt1Pnu7Tyd8rq6uF6KHrXHUwMDE0/lgtOPc123PrgX1X+Gi23zpB6PpcdTAwMWTYRfrfQ79cdTAwMTfU+iObUdRccv/4/fe2XHUwMDFktJyo69k1x7p1w57thVGv7vpWzW//7kZOO/yv+X9ot50/u367XHUwMDFlXHUwMDA1VnyQolN3Iz94PJbjOW2nXHUwMDEzhSD9f/B9dfXv/v+EdnXXbvuden94f0esXHUwMDFl13p066Hf6auKmWBMak3jXHUwMDExbrhcdTAwMDFHi5w67L5cdTAwMDaNnXiP2VRAZVxc/LZ3vr7+5UpUrtTa3XkobuPDXrueV45cdTAwMWW8R0PYtWYvcOK9YVx1MDAxNPgt59ytR01z9JHtg9+FPtgg/lXg91x1MDAxYc2OXHUwMDEzhkO/8bt2zY1cdTAwMWXMNoRcdTAwMDZb7U6jLyPeclx1MDAwZt+KmEhLIaQ5ZZxJitRgd19cdTAwMDDllmJKI0a1YFxu61x1MDAxMcVKvlx1MDAwNzNcdTAwMDGK/Yb6r1i1ql1rNUC/Tj1cdTAwMWWjSE3jxEnfPZ+uolx1MDAxNmOUXHUwMDEwSVx0WFx1MDAxY1x1MDAxMT5cdTAwMTjSdNxGMzJjXGJcdTAwMDE9mVCSP1x1MDAxZSphI6c/J1hJolx05yreY1x1MDAxNOju1vve8deoTZt20H2yXSE0X1x1MDAxMspcdTAwMWK9N0ddK+leiXnvKSEu9q/0fveb+vL15vtR8M0hXHUwMDAzWUO+XHUwMDE4OfdRYbDjn4+/xP4kYodGf8x6wKzaXHUwMDFlRrf35Vx1MDAwYreoP92cVnfOTlx1MDAxZZwvXHUwMDA3k7W1g8C/S8h9+lx1MDAxNPt+r1u3XHUwMDFmIVxmXHUwMDBiQVx1MDAxOeJEUarkYL/ndlqws9PzvHibX2vFqLeSUHhcZmyHzj+JtEhMRVrEXHUwMDE4w0igOOhnIW369C0u0pI0pFXCklx1MDAxY1x1MDAxM1x1MDAwMFn2ZqSNXHUwMDAyu1x1MDAxM3btXHUwMDAw4GtcdTAwMDLaytloS8bRXHUwMDE1SSFcYvxcIlZsbug6T/eMvcDvRGX3e9/FhMWxYohcYkTgmo64XHUwMDFjXHUwMDFhtWW3Xe9haGL7flxmmq91u1x1MDAxZv6TNHXogFxufZl8aPCa5zaMo1x1MDAxN2pwUk4wXHUwMDE0XHUwMDAzkVx1MDAwYtRlMKDt1utewlx1MDAxZmuggVxyMoPdLCzCXHUwMDBm3Ibbsb3KkIKpMfmIXHRcdTAwMTOCXHUwMDEyI6KmRaVcIkozJjDOXHUwMDFllKkotahBSYVFXGJFhHCFNNA9OVx1MDAxNJREUPBcdTAwMWNcdTAwMDAnJoFYYIJZflFpSawxVlxcXHRkXFyejFx1MDAwNyUjXHUwMDE2XHUwMDE1lFJcYlxcRVx1MDAxNUpQ0+dcdTAwMTiVmFx1MDAxMcpwwjczx2hf1dfG6Fx1MDAxMJq9IEbDyFx1MDAwZaJ1t1N3O1xy2Fx1MDAxOV/7nnl9lpjoR3GtZ7QsXCJcdTAwMGJcIlxcMiVcdTAwMTUgLONcdTAwMWMmViXGNeyusaOFsWCYXHUwMDFhsFVgcfI0YHBcdTAwMTUuOJ36bK22909Pd13S+7z9cNw624nOTsrlL5O0XHUwMDAypVx1MDAwMDypQDA/iiNcdTAwMDPwMeZcdTAwMGW00lx1MDAxNlKUXHUwMDBiialWiFA5ppRnh1HJb7fdXGKs/9l3O9GolfvmXFwz4d507ProXjip5L5RXFzoXHUwMDFhicNkN/60XHUwMDFhx03/y+DzX1x1MDAxZieOLk51Z/NcdTAwMWFz5FjcSvJ9XHUwMDFholx1MDAwNU4teoznXHSoRijlo5ufUY0gpYngmtLMsJY+y4tcbmuYUEsyXG54QJnJ31hsXHUwMDEyI4FiZmkuIaWD2Vx1MDAxMZrlSDZETPxcdTAwMDZApmKweEIuXHKhylx1MDAxMCM5kIs0Zu2fXHUwMDE3T2+6p9WOOjhu3n1cdTAwMGbsi1wiZ29PL7pF50SSXHLt7jKt9nuHLba9s5eNsKfKPd87u707YFx1MDAxYp9cdTAwMGVcdTAwMDJe334gPcI2xFx1MDAxY+SS8/ruzlar9kmtMVxcaXtHm52vjTnIzcm8yyW2Jc53b7e6J/iqXHUwMDE3Nlu3fFx1MDAwYl1cdTAwMWbW/3XGfUNcdTAwMGX7XHUwMDEzWSHqXu00Nq+0s1x1MDAxOVRxsX5cdTAwMWKG3l4wXHUwMDA3KyCnsvnJ82XjqHm/s394sulT725cdTAwMTGtO6tOMvmAsdhnejCVjDLNgcfH5Dmnelx1MDAwNrDsqSRcdTAwMDNcdTAwMGItXHT8yewkI93OXHUwMDBiSzKwSiVcdTAwMTlcdTAwMTRZ7H1IXHUwMDA2n0AyXHUwMDEySf5cdTAwMTPJgFx1MDAxY85cdTAwMTQxWDwv71DBeHRI/Fx1MDAwMoecXFzByFqxKD2XXHUwMDEzPlxcdsxcdTAwMGW3/uelWVx1MDAwM/H8xmXhsjO5mMHJkJxBrcJzrofd/0WljFx1MDAxOdR5tJQxU/PUSE3NXHQopmRauCpIXyVjQmaO1kP3XHUwMDA2bzU66/dcdTAwMDeoev39SFKvVKr/2GjlM4OVMG0pXHUwMDAyXHUwMDE5L1wiWFxuotVwsDJcdTAwMDU5XHUwMDE505JJzjWwcZFfsGo0XHUwMDFlrEqMXHUwMDA2K1NcdTAwMDIyYczeOSNwTnmVVuAlO8Xt1s3DWsA3rn5lXHUwMDA088pcYnIy73KJzSsjWC4r5JVcdTAwMTEsl1x1MDAxNTy9XlLV7Vx1MDAxMitcdOGUK/v3ja2teTD3nNTNK4FZlkmblb9MPmAs9unTXHUwMDBmz18oYdNbXySmlFx1MDAwYqqzM6J0Oy8sI2LpjEi+XHUwMDE3I1JcdTAwMTNcdTAwMTiRXHUwMDFjY0RKaES1UD91+rLei1wiv/PBbHvMXHUwMDAyLlx1MDAwYlx1MDAxN054Wfj4+O3WXHUwMDBlXFy7XHUwMDEzQWJcdTAwMTD2ajU4u+k5jVx1MDAxY1x1MDAxNj6nnGZcdTAwMDb3XHUwMDFmzWledzqpIT0j0Vx1MDAxMVPjmmjNMKTj2fss1Oej3fXjXHUwMDBi965q22F5u/LlU/PsfvHLXHUwMDEy1IJ4RVxcYoI1XHUwMDExmqmRuMaWwtysWULSQ4XKL64zZjpCaa6QeOe2tWtSo7h3Ujyon8pcdTAwMTLueC1ug9Bfmc6cMp2czLtcXGLzynSWy1xueWU6y2WFvDKd5bJCXis1y2KFWVx01ORcdTAwMDPGYp8+LUBcdTAwMDLFU1x1MDAxMijJJWGSZb93IN3Oi8q01FxmoiXei2hlS6BcdTAwMTjTjCgt/m1cdNShPyHhcFx1MDAwMF2C986eZiRcdTAwMTRcdTAwMTmyp1nnklx1MDAxYcxTO2FcdMJpy7mYMcp59rwpXHUwMDFk5Fx1MDAxNzWaibRcdTAwMTinlEqJXHUwMDA0I5QmXG5ccv1wRtzChGAlsemkppLnXHUwMDE3z1x1MDAxOFtCgFx1MDAxMkZcdTAwMWZcdTAwMDJQy8bDW3CLa66V6ZXUSGI6XHUwMDFh7Vx1MDAwMExCICXly6P99b2wL7/6JPSY1lx1MDAwYlx1MDAxYveRMkw5XHUwMDAxX6RiSmsrsTCYjVx1MDAxMDNCKMVoou8yS/fq0+DZnbCxTmBlXHUwMDBlXHQsolxmfFx1MDAwMsWoO9BcdCZcdTAwMTNpKbRcdTAwMDR9kIBJw9N0mlxmXHUwMDBmYzotUyPsdFc2rzEnjsWtJN9f3tqvp9d3KTiQubsxO56lV/1cdTAwMTdcdTAwMTXPqDbBgFx1MDAxOdicXHUwMDEygHBcdTAwMWRfQ1x1MDAxZvFMWFRcdTAwMGJcYiNNNVx1MDAxNXnecIOpXHUwMDA1XGZIK1x1MDAwNJPNpFBcdTAwMTN6+1x1MDAwNbGEpkhcdTAwMDBfUlx1MDAxMoGyXHR0fWIvXFxcIlx1MDAwZVx1MDAwMPOKXHUwMDA18Vx1MDAwNcWzXCKAXHUwMDA3NVxyOTA5nEgmkmD1iFx1MDAxZGA4XG5mU2ZBQlxinWgxylx0z4xORiWzJoBcdTAwMDG2cNySXHUwMDFjXHUwMDAzXHUwMDFhtYhAYFx1MDAxZkSxUlx1MDAxYatpOk0uXHUwMDE2LzWeXHUwMDE1pzuzeY278Vx1MDAwYlx1MDAxMS21uq3o9LtcYvshTjDJjmqVXHUwMDA3t1k+uHPuSifOt/Jaw1x0i9/2fyyq0VmgRihcdTAwMWVes1x1MDAxYcm4jPmxJNLgXHUwMDE5SvY0veZ2bVZcdTAwMTPONWdsXHUwMDFj0UhcIpmLK9sxTDzfj4SVuWmKklx1MDAxY+5HXHUwMDFh+NWEqkXt+vz4QNb3mq1cdTAwMWLvXHUwMDBivjj8fmJvbb29XHUwMDE4UpJHt7bT27mxT++i8yN6XHUwMDE4XHUwMDFlRvuTxb6odpOTunNcdTAwMTc7q3Yz+YCx2GdcdTAwMDBIudpcdTAwMDDyJlx1MDAxNk1yqt1IOb10g1x1MDAwMdZcdTAwMTBcdTAwMDeyllx1MDAxOUbSzbyoMFwiUmBcdTAwMDRi1sJcdFx1MDAxOHlcdTAwMTOKpDIjwifgXGJcdTAwMTlL5bBkXFzBgX5A4eZV1CdRuCFoaOugcFx1MDAxM5/Kc+HGuTU6WfvOw4eW8/DnZaFyWZhy67FcdTAwMWX68dxuPZ5xQVx1MDAxY63OTFb49Vd3nUjzx8KSYE2xTjzaY1ZYejdl9nnz/qHZ3tpu672d43DXX1/wsFx1MDAwNPSxuGKacHPNXHUwMDE0hI801Fx1MDAxM2KZZmVI/Vx1MDAxOXBmJN90N/LbL+8ms9LoVVx1MDAwYtdvubpfXHR0UdtZP946p3vVs/2Tln/8eWNxr+45qbssYmeRhslcdTAwMDfMqG2vWj/fYdXKzV6RrYn2UbVabspsc5aBjEhNaO5PRlE65W5lSLA4ZSp7TpM+fYuKejxcdTAwMTX1OLbk3FBvXHUwMDFldISYslx1MDAwMEf8XZ+EXHUwMDAy7kg5euuTUJaJjsy4gudNR8wjiaZGJqZKcYNcdTAwMGaZI3N9jbZcdTAwMTTfaWzfn5XOfMlC7yxoLXpcclx1MDAxNZtcdTAwMWGpXHUwMDEwXHUwMDFjklx1MDAwMY20XHUwMDEymlxyhSbm3JJcXHJwTZO64eRcIsyPICRcXDNO2KtcdTAwMTZ430JIPt/qYD3c6V03v29ffN3arGzc3PdcdTAwMTaXkOSk7nKJ3f3aq5xViN495Vx1MDAxN2frd07P35XeXCJcdTAwMWF3XHUwMDE2f5p8wFx1MDAxZs+fKFVEXHUwMDEynHsxXHUwMDA3J59cdTAwMDQ5XG7TXHUwMDE0XHUwMDAwQSRJ3CyUTp++RUVpjFNRWnGLzFxypedSz4G0kSMk87i5c55cdTAwMGW57Fxmalx1MDAwNueYXHUwMDAzg0p5ttz09WdJXHRcdTAwMTOcveDRcqkw9aKoJO9cdTAwMTaV5vZcItPywKnCiGAx0lx1MDAxZEeRtFx1MDAwNMdSIVxykavV9NuLXHUwMDFjIeVbgtI8fkxRhDnlXGaSK8YxmfC4XHUwMDA0XHUwMDAxWVx1MDAxNoP9XHUwMDE0eC2nVNCx9jmTnUlcdTAwMDRcdTAwMTDznlx1MDAxZDVPUfuq/rmMT5dLzzNWh5/jXHUwMDA2zF9rJMCI2DzCLNHN8bw4zCxBuVx1MDAxNppIhVx1MDAxOWj+3NPxwqfLpcfu6tCKNaJEKTgq4qbpR3MxrpayXHUwMDAwdDVkrlxcKUSVYGNaLdUqdJpP91x1MDAwN4y7cyxzJfn+Yr6RuJyNXHUwMDAwXHUwMDFiU5pcIk1Zdrpx4GyfXpVON4vnVcq/ke1cdTAwMTLZrp0sPLBJYVx1MDAxMbP2jzhcdTAwMDZGQYZxjUjz5DlOzJ2KXG6ihOXXJpgowMRkY6xvxviAIFi9a7VcdTAwMDbYXHUwMDE45TSvxaPxrt9qr1pNXHUwMDEygSS5UEOjs3byRn53XHUwMDFhr1x1MDAxODqLUVx1MDAxMvGkyWNsrTxFcMHudstcdTAwMTFYaFx1MDAwMHgwXHRu/ek0Y3mFW9e5W59QXHUwMDFhuO6/jNR+vJrIcMxcdTAwMTT8/c/KP/9cdTAwMDeoXHUwMDAzMlx1MDAwNiJ9 App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

    The App class is always the root of the DOM, so there is nowhere for the event to bubble to.

    "},{"location":"guide/events/#stopping-bubbling","title":"Stopping bubbling","text":"

    Event handlers may stop this bubble behavior by calling the stop() method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance when a text input widget responds to a key event it stops the bubbling so that the key doesn't also invoke a key binding.

    "},{"location":"guide/events/#custom-messages","title":"Custom messages","text":"

    You can create custom messages for your application that may be used in the same way as events (recall that events are simply messages reserved for use by Textual).

    The most common reason to do this is if you are building a custom widget and you need to inform a parent widget about a state change.

    Let's look at an example which defines a custom message. The following example creates color buttons which\u2014when clicked\u2014send a custom message.

    custom01.pyOutput custom01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.message import Message\nfrom textual.widgets import Static\n\n\nclass ColorButton(Static):\n    \"\"\"A color button.\"\"\"\n\n    class Selected(Message):\n        \"\"\"Color selected message.\"\"\"\n\n        def __init__(self, color: Color) -> None:\n            self.color = color\n            super().__init__()\n\n    def __init__(self, color: Color) -> None:\n        self.color = color\n        super().__init__()\n\n    def on_mount(self) -> None:\n        self.styles.margin = (1, 2)\n        self.styles.content_align = (\"center\", \"middle\")\n        self.styles.background = Color.parse(\"#ffffff33\")\n        self.styles.border = (\"tall\", self.color)\n\n    def on_click(self) -> None:\n        # The post_message method sends an event to be handled in the DOM\n        self.post_message(self.Selected(self.color))\n\n    def render(self) -> str:\n        return str(self.color)\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        yield ColorButton(Color.parse(\"#008080\"))\n        yield ColorButton(Color.parse(\"#808000\"))\n        yield ColorButton(Color.parse(\"#E9967A\"))\n        yield ColorButton(Color.parse(\"#121212\"))\n\n    def on_color_button_selected(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    ColorApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(0,\u00a0128,\u00a0128)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(128,\u00a0128,\u00a00)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(233,\u00a0150,\u00a0122)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(18,\u00a018,\u00a018)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the custom message class which extends Message. The constructor stores a color object which handler methods will be able to inspect.

    The message class is defined within the widget class itself. This is not strictly required but recommended, for these reasons:

    • It reduces the amount of imports. If you import ColorButton, you have access to the message class via ColorButton.Selected.
    • It creates a namespace for the handler. So rather than on_selected, the handler name becomes on_color_button_selected. This makes it less likely that your chosen name will clash with another message.
    "},{"location":"guide/events/#sending-messages","title":"Sending messages","text":"

    To send a message call the post_message() method. This will place a message on the widget's message queue and run any message handlers.

    It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a on_color_button_selected if it wanted to handle the message itself.

    "},{"location":"guide/events/#preventing-messages","title":"Preventing messages","text":"

    You can temporarily disable posting of messages of a particular type by calling prevent, which returns a context manager (used with Python's with keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed.

    The following example will play the terminal bell as you type. It does this by handling Input.Changed and calling bell(). There is a Clear button which sets the input's value to an empty string. This would normally also result in a Input.Changed event being sent (and the bell playing). Since we don't want the button to make a sound, the assignment to value is wrapped within a prevent context manager.

    Tip

    In reality, playing the terminal bell as you type would be very irritating -- we don't recommend it!

    prevent.pyOutput prevent.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Input\n\n\nclass PreventApp(App):\n    \"\"\"Demonstrates `prevent` context manager.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Input()\n        yield Button(\"Clear\", id=\"clear\")\n\n    def on_button_pressed(self) -> None:\n        \"\"\"Clear the text input.\"\"\"\n        input = self.query_one(Input)\n        with input.prevent(Input.Changed):  # (1)!\n            input.value = \"\"\n\n    def on_input_changed(self) -> None:\n        \"\"\"Called as the user types.\"\"\"\n        self.bell()  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = PreventApp()\n    app.run()\n
    1. Clear the input without sending an Input.Changed event.
    2. Plays the terminal sound when typing.

    PreventApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Clear \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/events/#message-handlers","title":"Message handlers","text":"

    Most of the logic in a Textual app will be written in message handlers. Let's explore handlers in more detail.

    "},{"location":"guide/events/#handler-naming","title":"Handler naming","text":"

    Textual uses the following scheme to map messages classes on to a Python method.

    • Start with \"on_\".
    • Add the message's namespace (if any) converted from CamelCase to snake_case plus an underscore \"_\".
    • Add the name of the class converted from CamelCase to snake_case.
    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa0/byFx1MDAxYf7eX1x1MDAxMeV8OStcdTAwMTV37pdKq1x1MDAxNbC0hZSKXHUwMDE2SludripjT1x1MDAxMlx1MDAxN8c29oTLVvz3845cdLGdXHUwMDFiJFx1MDAwNJaNXHUwMDA0iWcm9ut3nud5L86vXHUwMDE3rVbbXmWm/brVNpeBXHUwMDFmR2HuX7RfuvFzk1x1MDAxN1GawFx1MDAxNCmPi3SYXHUwMDA35cq+tVnx+tWrgZ+fXHUwMDFhm8V+YLzzqFx1MDAxOPpxYYdhlHpBOnhcdTAwMTVZMyj+cP8/+Fx1MDAwM/N7llx1MDAwZUKbe9VFNkxcdTAwMTjZNL+5lonNwCS2gLP/XHUwMDBmjlutX+X/mnW5XHSsn/RiU36hnKpcZqSMT45+SJPSWEyEIEghQcYrouJPuJ41IUx3wWZTzbih9uHRWXbGf36Sxu/wY3HOWYfH1WW7UVx1MDAxY1x1MDAxZtqruDSrSOFuqrnC5ump+Vx1MDAxMoW27649MT7+VuhcdTAwMTd9U/tanlx1MDAwZXv9xFx1MDAxNO7+0Xg0zfwgslfuRKhcdTAwMWG9cUJ93WXpXHUwMDAx5mkqJEOSY0mVqlx1MDAxY+JOQFx1MDAxNfVcdTAwMTRiWlx1MDAxMy2xkpLwXHTTttNcdTAwMTj2XHUwMDAyTPtcdTAwMGYqX5VtJ35w2lx1MDAwM1x1MDAwM5OwWtP1OeHg2GrVxeiWufaI1FIqxqjQRHAxXtI3Ua9vYVxymMqphlx1MDAxZKlcdTAwMTlhyt2QREmMqVx1MDAxYY+7XHUwMDBiZ7thiYu/Jr3Z9/Ns5LR2aWDNaHe4U1x1MDAwM1X15WFcdTAwMTb6N3uPhaCMYiYwl3Q8XHUwMDFmR8kpTCbDOK7G0uC0gks5ev1yXHUwMDA1nHIh5uGUaoKpVETcXHUwMDFiprvKbH3K2LtT8/VL9mZjiFx1MDAwZnW8/8xhypDwsFx1MDAwMjcgXHUwMDAxb0hKOVx1MDAwMVPmwYZIqplmgmPxIJRicqKUmIVSgpCHXHUwMDA1lkpywrRAWC2DUsw1sEgyjNaP09FEXHUwMDA1rNqGZ2pPXHUwMDA1vv3z8it786M3/FZcdTAwMWO9XHUwMDFkRuNzNVDo53l60Vx1MDAxZc9cXI8+zWdcdTAwMDHilMHNPlx0XHUwMDBiXHUwMDE0V/NYoCRBVFx1MDAwYszuzYKtg86eke92XHUwMDBlXHUwMDA2W+Kk0z00XHUwMDE355vZM2eBQMqTXHUwMDFjIaEoXHUwMDA1KFwiPEVcdTAwMDLFXGIjXHUwMDFhlNyJwsNYwFx1MDAwMmG6fFx1MDAxNlx1MDAwYjDhXHUwMDFlgFx1MDAxOPM6xu+Bf65cdTAwMTQhgj6GTC+C//bnz2FHXuxcdTAwMDdcdTAwMWb3tnl+2T/svDtGa4M/k6qGulx1MDAwN8Lfmks7XHUwMDBi+Vx1MDAxOOm5XHUwMDAxXHUwMDAwXHUwMDEyXHUwMDE1XGKcXGYtgX1fXHUwMDFl51x1MDAwN/tZxFx1MDAwZtOf4uJIbnx7n+C1Yn/iW3Xo45WgT1x1MDAxMfVcdTAwMTjSWiHIWIRqyj+XIMtcdTAwMWMxqTlnXHUwMDFhXHUwMDA06bGAr+g03jmaxLnETElIY8TyOC/cwYo4XHUwMDE3Nk63szOqTj7GfPPqfNOc7H5dXHUwMDEzzlx0oYIqslx1MDAwNM4rNKWJPYz+NmX4bIy+8Vx1MDAwN1F81YBESVx1MDAwMDBw3z81Rcv2TWtgbD9ccr8nPnwqXG6/Z1p9P1x0Y5PXN7EwYJC7XHUwMDAyo41TbcZRz/GnXHUwMDFkm26TWDaCemI8bdOa01x1MDAwMzDNh9Plu+HkLaZ51ItcdTAwMTI/PlrCzJVcYi/lXFy+Q6AjIFx1MDAwN7yWRdzF9zefd97KLOb9y1x1MDAxZPb+7eCg8377p37efGeMe1x1MDAwNFx1MDAwM6khmDHBVDPUQcrhQSjhXHUwMDA0SSYg4j1apNOVqC4gPKFgiUtcdTAwMGKflvCPmddcdTAwMTFcdTAwMDI7oKpcdTAwMWJ6dMKPWJNAzV9cdTAwMDBOzPfkv+nQmrxcdTAwMTXEflH8tlx1MDAxNNtcdTAwMDPwXl0g1sf3u6xcXInsXHUwMDAyycnRMdlcdTAwMTXVUNjo+3M97n56XHUwMDFmn3X2985+fOufpztcdTAwMWaO9o7X24RYO9dcdTAwMDUlnlx1MDAwMFx1MDAxYbuyTihBmlxc54J6RFx1MDAxMq2g2Fx1MDAwNtFTXHUwMDBm60As4DqrrruA61hLXHJ/XHUwMDFjVcHwScj+mFksRHdcdTAwMDVC+2Rkd429Vtr9ntzGypI9z4Pic2xbSOxcdTAwMWKHz6pYiZ5cdTAwMWO9ZTaTpKyH7t9eXFyc3y3BbDKJ0Vx1MDAxNZkt70zahfa05sQ1ypieiOGcS09Csq6ohDBcdTAwMGbEnsvrUDOFuqtXq641XHUwMDA0nlaUKC2FnNFZxIR6XHUwMDFhMSpcdTAwMTVcdTAwMThcdTAwMDJcdTAwMDU0q1x1MDAwNPm2duVQ5nGxXHUwMDAy6VfvMN4k3ct0XHUwMDE4a3b4ud2KkjBKejBZ6cltx3z3XHUwMDFlhWBJ5GDorNxAXHUwMDFlRYxzjClcdTAwMTJQ84Jtsras52cjT3NcdTAwMDFcdTAwMWJcdTAwMGVBS1x1MDAxM4bxaMH12CyThHdcdTAwMWLV/Vx1MDAxNvTzqyP68biTXHUwMDExxTa2gy8mnmVcdTAwMTTyXHUwMDE0XHUwMDE4xFx1MDAxMFx1MDAxNH1cdTAwMTIkWVA9bVx1MDAxMvdcdTAwMTBsLFx1MDAxM6497HrYesomoLfdTlx1MDAwN4PIgu9cdTAwMGbSKLGTPi6duek43jf+lIDAPdXnJsUgc2dsqnv1qVVcdTAwMTGmPFx1MDAxOH/+6+XM1fOx7F5cdTAwMWLTMK5O+KL+vrSQQVx1MDAxMEeTw5WSXHTYdCnv339YnLg+RyXjWHuuv4ggzVx1MDAwN6+z6sJlOaKQx6FcIlx1MDAwM4oghvWChyQ80FxmhatKXHUwMDE5XHUwMDA1XHUwMDFiXHUwMDE4p5hcbsY10bXYUXXfqMcoXHUwMDE0RIpgxFx1MDAxMVx1MDAxMzVVXHUwMDFk5S+YKIFcdTAwMTnI8dNKXHUwMDE5g1x1MDAwZlUwXFy/lC2ucZtS5kpoTDjDXG70SkheY9FINzQkpJhqXHUwMDA0rlx1MDAwNDditpqSLX7S0rRcdEmCXHUwMDA1lLRKYoqRXHUwMDE0fMomXHUwMDA1abBEXHUwMDAyYVxydkGWPG3Uv0nKNuaCuZydwvHalIyrudVcdTAwMTZWIJ5Y1blxl5QtTsv/XHUwMDAxKVN3VltaeKrsW0OgRnDHXHUwMDEzWZlcdTAwMDAoUijFXHUwMDFj7sX8xlxubFs3kKsqXHUwMDE5cTVcdTAwMWRkN1x1MDAxYVx1MDAwNFWDINVcdTAwMTKuKinDwnNSi7iCilx1MDAwYik5lZRp95iB1DvfT5OVrVos3VPKXHUwMDE2l/CNXHUwMDA0XGLCsqZcXFNcbuVcdTAwMDSkZOCQXHUwMDFhjUa6IT1QXGbwn1x1MDAwMGAzwlx1MDAxOF1NzFx1MDAxNj8wa1olmGBaXHUwMDBipIWSkFxyztJXXHL7XHUwMDBmlTSDRIVCMf3v1rL5cC6np5G8pJrN61x1MDAxY1x1MDAxMTz3iSiBbFx1MDAwNMKJXFyidbQ48W5qWd9cdTAwMGb6w9zMU7N1NY/0nSUmV56mXHUwMDAwJii1QdpcdTAwMTVrylx1MDAxOVXSQ1gypLSm5IE/XGawuZ9cdTAwMTSZn1x1MDAwMyVm5GZcdTAwMTJcIlx1MDAxNilcdTAwMDFfnmlGbkaxJ1x1MDAxNSbAjNGrZs2oyoRbgJJ4lVx1MDAxZlxiPNcnR5hJcEu1tyv2loTHXHUwMDE0XHUwMDE0Nje+hZdsrFx1MDAxYfeamt1cImdwmvxcYtxcdTAwMDb+OFx1MDAxOVpcdTAwMGJcdTAwMDdcdTAwMDVQILCmkYOPu02Ez92hp3icNNfWXHUwMDE3t74u/dz2s+zQgpfHalxyMInCkauqK7TPI3OxNetnWOXLnbVcdTAwMTRcdTAwMWPHbONA8uv6xfX/XHUwMDAx2ibQXHUwMDAzIn0= Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected

    Messages have a namespace if they are defined as a child class of a Widget. The namespace is the name of the parent class. For instance, the builtin Input class defines it's Changed message as follow:

    class Input(Widget):\n    ...\n    class Changed(Message):\n        \"\"\"Posted when the value changes.\"\"\"\n        ...\n

    Because Changed is a child class of Input, its namespace will be \"input\" (and the handler name will be on_input_changed). This allows you to have similarly named events, without clashing event handler names.

    Tip

    If you are ever in doubt about what the handler name should be for a given event, print the handler_name class variable for your event class.

    Here's how you would check the handler name for the Input.Changed event:

    >>> from textual.widgets import Input\n>>> Input.Changed.handler_name\n'on_input_changed'\n
    "},{"location":"guide/events/#on-decorator","title":"On decorator","text":"

    In addition to the naming convention, message handlers may be created with the on decorator, which turns a method into a handler for the given message or event.

    For instance, the two methods declared below are equivalent:

    @on(Button.Pressed)\ndef handle_button_pressed(self):\n    ...\n\ndef on_button_pressed(self):\n    ...\n

    While this allows you to name your method handlers anything you want, the main advantage of the decorator approach over the naming convention is that you can specify which widget(s) you want to handle messages for.

    Let's first explore where this can be useful. In the following example we have three buttons, each of which does something different; one plays the bell, one toggles dark mode, and the other quits the app.

    on_decorator01.pyOutput on_decorator01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n        \"\"\"Handle all button pressed events.\"\"\"\n        if event.button.id == \"bell\":\n            self.bell()\n        elif event.button.has_class(\"toggle\", \"dark\"):\n            self.dark = not self.dark\n        elif event.button.id == \"quit\":\n            self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    1. The message handler is called when any button is pressed

    OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Note how the message handler has a chained if statement to match the action to the button. While this works just fine, it can be a little hard to follow when the number of buttons grows.

    The on decorator takes a CSS selector in addition to the event type which will be used to select which controls the handler should work with. We can use this to write a handler per control rather than manage them all in a single handler.

    The following example uses the decorator approach to write individual message handlers for each of the three buttons:

    on_decorator02.pyOutput on_decorator02.py
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    @on(Button.Pressed, \"#bell\")  # (1)!\n    def play_bell(self):\n        \"\"\"Called when the bell button is pressed.\"\"\"\n        self.bell()\n\n    @on(Button.Pressed, \".toggle.dark\")  # (2)!\n    def toggle_dark(self):\n        \"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\n        self.dark = not self.dark\n\n    @on(Button.Pressed, \"#quit\")  # (3)!\n    def quit(self):\n        \"\"\"Called when the quit button is pressed.\"\"\"\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    While there are a few more lines of code, it is clearer what will happen when you click any given button.

    Note that the decorator requires that the message class has a control property which should return the widget associated with the message. Messages from builtin controls will have this attribute, but you may need to add a control property to any custom messages you write.

    Note

    If multiple decorated handlers match the message, then they will all be called in the order they are defined.

    The naming convention handler will be called after any decorated handlers.

    "},{"location":"guide/events/#applying-css-selectors-to-arbitrary-attributes","title":"Applying CSS selectors to arbitrary attributes","text":"

    The on decorator also accepts selectors as keyword arguments that may be used to match other attributes in a Message, provided those attributes are in Message.ALLOW_SELECTOR_MATCH.

    The snippet below shows how to match the message TabbedContent.TabActivated only when the tab with id home was activated:

    @on(TabbedContent.TabActivated, tab=\"#home\")\ndef home_tab(self) -> None:\n    self.log(\"Switched back to home tab.\")\n    ...\n
    "},{"location":"guide/events/#handler-arguments","title":"Handler arguments","text":"

    Message handler methods can be written with or without a positional argument. If you add a positional argument, Textual will call the handler with the event object. The following handler (taken from custom01.py above) contains a message parameter. The body of the code makes use of the message to set a preset color.

        def on_color_button_selected(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n

    A similar handler can be written using the decorator on:

        @on(ColorButton.Selected)\n    def animate_background_color(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n

    If the body of your handler doesn't require any information in the message you can omit it from the method signature. If we just want to play a bell noise when the button is clicked, we could write our handler like this:

        def on_color_button_selected(self) -> None:\n        self.app.bell()\n

    This pattern is a convenience that saves writing out a parameter that may not be used.

    "},{"location":"guide/events/#async-handlers","title":"Async handlers","text":"

    Message handlers may be coroutines. If you prefix your handlers with the async keyword, Textual will await them. This lets your handler use the await keyword for asynchronous APIs.

    If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long as handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use.

    Info

    To re-use the chef analogy, if an order comes in for beef wellington (which takes a while to cook), orders may start to pile up and customers may have to wait for their meal. The solution would be to have another chef work on the wellington while the first chef picks up new orders.

    Network access is a common cause of slow handlers. If you try to retrieve a file from the internet, the message handler may take anything up to a few seconds to return, which would prevent the widget or app from updating during that time. The solution is to launch a new asyncio task to do the network task in the background.

    Let's look at an example which looks up word definitions from an api as you type.

    Note

    You will need to install httpx with pip install httpx to run this example.

    dictionary.pydictionary.tcssOutput dictionary.py
    import asyncio\n\ntry:\n    import httpx\nexcept ImportError:\n    raise ImportError(\"Please install httpx with 'pip install httpx' \")\n\nfrom rich.json import JSON\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass DictionaryApp(App):\n    \"\"\"Searches a dictionary API as-you-type.\"\"\"\n\n    CSS_PATH = \"dictionary.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Search for a word\")\n        yield VerticalScroll(Static(id=\"results\"), id=\"results-container\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"A coroutine to handle a text changed message.\"\"\"\n        if message.value:\n            # Look up the word in the background\n            asyncio.create_task(self.lookup_word(message.value))\n        else:\n            # Clear the results\n            self.query_one(\"#results\", Static).update()\n\n    async def lookup_word(self, word: str) -> None:\n        \"\"\"Looks up a word.\"\"\"\n        url = f\"https://api.dictionaryapi.dev/api/v2/entries/en/{word}\"\n        async with httpx.AsyncClient() as client:\n            results = (await client.get(url)).text\n\n        if word == self.query_one(Input).value:\n            self.query_one(\"#results\", Static).update(JSON(results))\n\n\nif __name__ == \"__main__\":\n    app = DictionaryApp()\n    app.run()\n
    dictionary.tcss
    Screen {\n    background: $panel;\n}\n\nInput {\n    dock: top;\n    width: 100%;\n    height: 1;\n    padding: 0 1;\n    margin: 1 1 0 1;\n}\n\n#results {\n    width: auto;\n    min-height: 100%;\n}\n\n#results-container {\n    background: $background 50%;\n    overflow: auto;\n    margin: 1 2;\n    height: 100%;\n}\n

    DictionaryApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the highlighted line in the above code which calls asyncio.create_task to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

    "},{"location":"guide/input/","title":"Input","text":"

    This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.

    Quote

    More Input!

    \u2014 Johnny Five

    "},{"location":"guide/input/#keyboard-input","title":"Keyboard input","text":"

    The most fundamental way to receive input is via Key events which are sent to your app when the user presses a key. Let's write an app to show key events as you type.

    key01.pyOutput key01.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n\n    def on_key(self, event: events.Key) -> None:\n        self.query_one(RichLog).write(event)\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    InputApp Key(key='T',\u00a0character='T',\u00a0name='upper_t',\u00a0is_printable=True) Key(key='e',\u00a0character='e',\u00a0name='e',\u00a0is_printable=True) Key(key='x',\u00a0character='x',\u00a0name='x',\u00a0is_printable=True) Key(key='t',\u00a0character='t',\u00a0name='t',\u00a0is_printable=True) Key(key='u',\u00a0character='u',\u00a0name='u',\u00a0is_printable=True) Key(key='a',\u00a0character='a',\u00a0name='a',\u00a0is_printable=True) Key(key='l',\u00a0character='l',\u00a0name='l',\u00a0is_printable=True) Key( key='exclamation_mark', character='!', name='exclamation_mark', is_printable=True )

    When you press a key, the app will receive the event and write it to a RichLog widget. Try pressing a few keys to see what happens.

    Tip

    For a more feature rich version of this example, run textual keys from the command line.

    "},{"location":"guide/input/#key-event","title":"Key Event","text":"

    The key event contains the following attributes which your app can use to know how to respond.

    "},{"location":"guide/input/#key","title":"key","text":"

    The key attribute is a string which identifies the key that was pressed. The value of key will be a single character for letters and numbers, or a longer identifier for other keys.

    Some keys may be combined with the Shift key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the key attribute will be prefixed with shift+. For example, Shift+Home will produce an event with key=\"shift+home\".

    Many keys can also be combined with Ctrl which will prefix the key with ctrl+. For instance, Ctrl+P will produce an event with key=\"ctrl+p\".

    Warning

    Not all keys combinations are supported in terminals and some keys may be intercepted by your OS. If in doubt, run textual keys from the command line.

    "},{"location":"guide/input/#character","title":"character","text":"

    If the key has an associated printable character, then character will contain a string with a single Unicode character. If there is no printable character for the key (such as for function keys) then character will be None.

    For example the P key will produce character=\"p\" but F2 will produce character=None.

    "},{"location":"guide/input/#name","title":"name","text":"

    The name attribute is similar to key but, unlike key, is guaranteed to be valid within a Python function name. Textual derives name from the key attribute by lower casing it and replacing + with _. Upper case letters are prefixed with upper_ to distinguish them from lower case names.

    For example, Ctrl+P produces name=\"ctrl_p\" and Shift+P produces name=\"upper_p\".

    "},{"location":"guide/input/#is_printable","title":"is_printable","text":"

    The is_printable attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If is_printable is False then the key is a control code or function key that you wouldn't expect to produce anything in an input.

    "},{"location":"guide/input/#aliases","title":"aliases","text":"

    Some keys or combinations of keys can produce the same event. For instance, the Tab key is indistinguishable from Ctrl+I in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of Tab, the aliases attribute will contain [\"tab\", \"ctrl+i\"]

    "},{"location":"guide/input/#key-methods","title":"Key methods","text":"

    Textual offers a convenient way of handling specific keys. If you create a method beginning with key_ followed by the key name (the event's name attribute), then that method will be called in response to the key press.

    Let's add a key method to the example code.

    key02.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n\n    def on_key(self, event: events.Key) -> None:\n        self.query_one(RichLog).write(event)\n\n    def key_space(self) -> None:\n        self.bell()\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    Note the addition of a key_space method which is called in response to the space key, and plays the terminal bell noise.

    Note

    Consider key methods to be a convenience for experimenting with Textual features. In nearly all cases, key bindings and actions are preferable.

    "},{"location":"guide/input/#input-focus","title":"Input focus","text":"

    Only a single widget may receive key events at a time. The widget which is actively receiving key events is said to have input focus.

    The following example shows how focus works in practice.

    key03.pykey03.tcssOutput key03.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass KeyLogger(RichLog):\n    def on_key(self, event: events.Key) -> None:\n        self.write(event)\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    CSS_PATH = \"key03.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield KeyLogger()\n        yield KeyLogger()\n        yield KeyLogger()\n        yield KeyLogger()\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n
    key03.tcss
    Screen {\n    layout: grid;\n    grid-size: 2 2;\n    grid-columns: 1fr;\n}\n\nKeyLogger {\n    border: blank;\n}\n\nKeyLogger:hover {\n    border: wide $secondary;\n}\n\nKeyLogger:focus {\n    border: wide $accent;\n}\n

    InputApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 name='o',\u258echaracter='d',\u258a is_printable=True\u258ename='d',\u258a )\u258eis_printable=True\u258a Key(\u258e)\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u2586\u2586\u258ename='exclamation_mark',\u2587\u2587\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\u258a )\u258e)\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    The app splits the screen in to quarters, with a RichLog widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.

    Tip

    the :focus CSS pseudo-selector can be used to apply a style to the focused widget.

    You can move focus by pressing the Tab key to focus the next widget. Pressing Shift+Tab moves the focus in the opposite direction.

    "},{"location":"guide/input/#controlling-focus","title":"Controlling focus","text":"

    Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's focus() method.

    "},{"location":"guide/input/#focus-events","title":"Focus events","text":"

    When a widget receives focus, it is sent a Focus event. When a widget loses focus it is sent a Blur event.

    "},{"location":"guide/input/#bindings","title":"Bindings","text":"

    Keys may be associated with actions for a given widget. This association is known as a key binding.

    To create bindings, add a BINDINGS class variable to your app or widget. This should be a list of tuples of three strings. The first value is the key, the second is the action, the third value is a short human readable description.

    The following example binds the keys R, G, and B to an action which adds a bar widget to the screen.

    binding01.pybinding01.tcssOutput binding01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Footer, Static\n\n\nclass Bar(Static):\n    pass\n\n\nclass BindingApp(App):\n    CSS_PATH = \"binding01.tcss\"\n    BINDINGS = [\n        (\"r\", \"add_bar('red')\", \"Add Red\"),\n        (\"g\", \"add_bar('green')\", \"Add Green\"),\n        (\"b\", \"add_bar('blue')\", \"Add Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Footer()\n\n    def action_add_bar(self, color: str) -> None:\n        bar = Bar(color)\n        bar.styles.background = Color.parse(color).with_alpha(0.5)\n        self.mount(bar)\n        self.call_after_refresh(self.screen.scroll_end, animate=False)\n\n\nif __name__ == \"__main__\":\n    app = BindingApp()\n    app.run()\n
    binding01.tcss
    Bar {\n    height: 5;\n    content-align: center middle;\n    text-style: bold;\n    margin: 1 2;\n    color: $text;\n}\n

    BindingApp red\u2582\u2582 green blue blue \u00a0R\u00a0\u00a0Add\u00a0Red\u00a0\u00a0G\u00a0\u00a0Add\u00a0Green\u00a0\u00a0B\u00a0\u00a0Add\u00a0Blue\u00a0

    Note how the footer displays bindings and makes them clickable.

    Tip

    Multiple keys can be bound to a single action by comma-separating them. For example, (\"r,t\", \"add_bar('red')\", \"Add Red\") means both R and T are bound to add_bar('red').

    "},{"location":"guide/input/#binding-class","title":"Binding class","text":"

    The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a Binding instance which exposes a few more options.

    "},{"location":"guide/input/#priority-bindings","title":"Priority bindings","text":"

    Individual bindings may be marked as a priority, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget.

    You can create priority key bindings by setting priority=True on the Binding object. Textual uses this feature to add a default binding for Ctrl+C so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority:

        BINDINGS = [\n        Binding(\"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True),\n        Binding(\"tab\", \"focus_next\", \"Focus Next\", show=False),\n        Binding(\"shift+tab\", \"focus_previous\", \"Focus Previous\", show=False),\n    ]\n
    "},{"location":"guide/input/#show-bindings","title":"Show bindings","text":"

    The footer widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set show=False. The default bindings on App do this so that the standard Ctrl+C, Tab and Shift+Tab bindings don't typically appear in the footer.

    "},{"location":"guide/input/#mouse-input","title":"Mouse Input","text":"

    Textual will send events in response to mouse movement and mouse clicks. These events contain the coordinates of the mouse cursor relative to the terminal or widget.

    Information

    The trackpad (and possibly other pointer devices) are treated the same as the mouse in terminals.

    Terminal coordinates are given by a pair values named x and y. The X coordinate is an offset in characters, extending from the left to the right of the screen. The Y coordinate is an offset in lines, extending from the top of the screen to the bottom.

    Coordinates may be relative to the screen, so (0, 0) would be the top left of the screen. Coordinates may also be relative to a widget, where (0, 0) would be the top left of the widget itself.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ba0/bSFx1MDAxNP3eX1x1MDAxMaVfdqUynfej0mpcdTAwMDVcdTAwMDGaQHktXHUwMDAxXG6rVeUmTmxw7GA7XHUwMDA0qPjve+1A7DxcdKHJhlXzXHUwMDAxyIxcdTAwMWbXM+ecOfeO+fGuUCjGd227+KlQtG9rlufWQ6tb/JC039hh5Fx1MDAwNj500fR7XHUwMDE0dMJaeqRcdTAwMTPH7ejTx48tK7yy47Zn1Wx040ZcdTAwMWTLi+JO3VxyUC1ofXRju1x1MDAxNf2Z/Ny3WvZcdTAwMWbtoFWPQ5TdZM2uu3FcdTAwMTD27mV7dsv241xirv43fC9cdTAwMTR+pD9z0YV2Lbb8pmenJ6RdWYBcdTAwMDRcdTAwMTM63LxcdTAwMWb4abREcIO1xFxc9I9wo024YWzXobtcdTAwMDFB21lP0lRcXN/ddcqh3GlFe7dcdTAwMDd1p4nXP5Nqdt+G63nH8Z2XxlVcdTAwMGKDKFpzrLjmZEdEcVx1MDAxOFxc2WduPXaehi/X3j83XG5gKLKzwqDTdHw7SkaB9FuDtlVz47v0KXG/tTdcdTAwMTSfXG5Zyy18Y1xcIS1cdTAwMTVcdTAwMTFEsuShs0dOzqdKIK2l4ZphTnVuQHpxlVx1MDAwMlx1MDAwZuZcdTAwMDPieo/TT1x1MDAxNtl3q3bVhPD8ev+YOLT8qG2FMGvZcd3HJ1x1MDAxNpwjySjjXGZcdTAwMGJCSHYjx3abTpxEKjHSXHUwMDA0XHUwMDFiziVjgmgqs2DsdGKMVIJjzni/I4mgXamnIPlneExcdTAwMWQrbD+OXTFKvuSiT1x1MDAwMt/KISw7udOuWz1cdTAwMWNcdTAwMTApOVFKJSGpfr/n+lfQ6Xc8L2tcdTAwMGJqV1x1MDAxOXTS1odcdTAwMGZzgFZRPFx0s1RcdTAwMWKYKMrUzJC9pXW+tS3Pr02zVbnc2zrnNz6ZXHUwMDAw2SHY/ZdgXHUwMDE1XHUwMDE4XHUwMDBiLFx1MDAxODbKaDlcdTAwMDJWhinBXG5LSoyii0QrQ5hIQbVQRFx1MDAxOaVH4Uo1klx1MDAxMjNcdTAwMGU6QiVcdTAwMDNoj8BVSmNcYjVcdTAwMDS/Ybjanue2o7FglUJMXHUwMDAyq1x1MDAxMdhcYi3MzFg9XzPfhFPdPmp2tXdcdTAwMTFEe/5e4/M8WCXLw6owiFx1MDAxOclcdTAwMTnAQ1x1MDAxYWL0IFa1QEJcdTAwMDEoXHUwMDE4pVx1MDAxOGuB+Wuw+r5hXHQq6ChOXHRDXHUwMDFjUyqMpPCLa81HgUooXHUwMDEyXHUwMDAwXHJcdTAwMDOaiiljNFx1MDAwN45HoFxuiFx1MDAxNEiVQ/D/XG6ocKOJToBcdTAwMTgjYU0hbGaofnXx93XiXHUwMDFj8Z3908PWXHUwMDA2/evM7norXHUwMDBlVS1cdTAwMTGjilx1MDAxOSap1ppcdTAwMTA1hFWOXHUwMDAwXHUwMDFjSmiDXHUwMDA11yQnu/Nh9TtI+KKwSlx1MDAxOSxcZkyI/6moKskmYlx1MDAxNVx1MDAxNlx1MDAxYWpcdTAwMTjFs2M12rwpy3W3s3dcdTAwMWWas9qFiNbL5Hi1scpJXHUwMDAyRlx1MDAwNoNONFx1MDAwMJaTIagypDBcdTAwMDYzZFx1MDAxOKgue5VcdTAwMDN4T+h38FSLQiphXHSjXHUwMDE4e8tItcIw6I5Nr9jExZ8rpjCTuVx1MDAwNfE5mKqD+0atpiW/3qVhyexcdTAwMTFIXHUwMDAzK1x1MDAxM2DqWDWnXHUwMDEz2v89UJlUSEkh6WBGxVx1MDAxOEFcdTAwMTgyLblAd4pcdTAwMTFVXG4yKTUmi5JitPNcdJDwUMJoMUf6lFx1MDAwNjcnILlcdTAwMDBcdTAwMWL9XHUwMDAyQObisMJ4w/Xrrt9cdTAwMWM+xfbrWc9cdTAwMTNsXHUwMDBi/apBpecqO9s7+5ub3Vx1MDAxM6dy24lOon1cdTAwMTmfZrhKkFx1MDAxNdQ6UTqghFx1MDAxOUHBrFx1MDAwM+UlOFx1MDAwMpI7qGm1XHUwMDEzVCNB01F97HjIoreiuFx1MDAxNLRablxmz31cdTAwMTi4fjxcdTAwMWNs+iDrXHSVXHUwMDFj26qPeZR83zDn2slcdTAwMTWzKkjyyf4qZKBMv/T//ufD2KPXRqGTfHKgya7wLv/7xVx1MDAwMqHkcGM/k4XEXG6QSNTsXHUwMDAyXHUwMDEx3G59bVxcnljd06tSuXFz0vWv/7pYfYGAXGZcdTAwMTFWMTUkXHUwMDEw1CDJsVx1MDAwNpVkXHUwMDFhw2rOhlwi+olZrEGQepixOkFcdTAwMTDBZEC9nlRCc2a4WrZMXHUwMDE4pvNcdP0yZeLwRl+FR+t39dYhO7mp4jiu7tTHy1x1MDAwNCZcdTAwMTTUjIO6q0RLiaa5w3pCQTCSvZF900oxip3ks9aHzVx1MDAwYnVcIrZv43EykUPZkExcYkGYJHmj/5xKTJ/HXHUwMDE1VVx0zjT43Vx1MDAwMY6mKkFcdTAwMDTSSi/WR+Sy3qysNSpcYlx1MDAwMGdcIlx1MDAxOPD051x1MDAxYtk+in7kQDaT6Fx1MDAwZqCrR4R+z8NcdTAwMTMkp7lcdTAwMTLKSTZjL5CbRuDHx+59byVcdTAwMWJo3bZarnc3gIRcdTAwMTT2SdEgP0mRXHK3SzM6PXDguuc2XHUwMDEzTlx1MDAxND27MUiW2K1ZXr87XHUwMDBlckNag1x1MDAxYltwubAyXCJcdTAwMTdB6DZd3/Kq/SDmoqiavI+iXHLVIIM4O+LZQt9US7aiXHUwMDFjXHUwMDA1XHUwMDFkQorgXHUwMDExknKMXHUwMDExJKl60Gz/bJJmsUwjqUnqO1xc5EzVUkg6PXVcdTAwMWLA1zwknTd1mIukd6tA0rvpJJ26fcTYRNMtlZGJY8mW2+eYKr/qzeq5+ra5xaPTXHUwMDFhoSdl91x1MDAxMs/H1OVtIFx0IdBwRs45RYxyOuDE5yps1pVNOOejXHUwMDFjZYlcdTAwMTCMddnSoLEuW1HCtFFkuVuZWsFgyFx1MDAxN1x1MDAxMGoqXHUwMDE2J+Z+Uk0sXHUwMDBlJU5CQs7D5cxA1MI76HSFdqtB6aLEq3LvXHUwMDFiu1xc9SVcdTAwMDOEXHUwMDE4XHQ96uu4ZFxic2rkK8G4iPpQsrPKwOjllvNlZH5cdTAwMWFzpfBcdTAwMGJA+fMyv/tNZ9uznS1TvdhVpVp5o0qP4tzq9atA9PhZQIFIYjXc2lx1MDAxN1x0WMhcYoXRnl0kqvbpl8P7vc+4XHUwMDFhhpVyffdr4/42Wn2RMIhcdTAwMWI6Ulwi4onfZJpqsshXXHUwMDFj5ilcdTAwMGVRXGYzwzWZx2a+TYnoVlx1MDAwZU7Lbjlyb732l42Se3h3fXw1oTiEXHUwMDA12Fx1MDAwM5gzpmBhXHUwMDE3ODd7hV/VodzDzpx6MjptN5Qkb4O9IPecPpUrqlx1MDAxMZKDg1SDfqHnalx1MDAxNdJcdTAwMGKWiNnqQzrZc9RCLcDK9mE0JvOcrvhcdTAwMDPwennmXHSCkzexv8pDUzia26JcdTAwMWbmqOGgXHUwMDExlJDZs87pjmxFOSpcdTAwMTRF4JuH8k7BKFqF0pDSsCwl3nS5/JyetlxyQGvl+flcdTAwMTYqQ5P4SfFEfjLNXHUwMDA1y7+g8lx1MDAxYztcdTAwMWT7vGxFYSm4a1xcXHUwMDFjXHUwMDFkXHUwMDE4vGNcImfl92HpSFxyJt1gwVx1MDAwNqnXloSeMdizsFx1MDAxM1wiXHUwMDEz4PfZkl++1FxmgzYtjUC/4Vx1MDAwZlx1MDAwNfz7KrDoMZK5qKRy7zRcctdXXHUwMDE5JpqJXHUwMDE3uNHKmXW4WflyXHUwMDE4XHUwMDFknYlcbrlukMvd49aqc4lrgyC/XHUwMDE53JZM7SjTi99cbpmRUZxcdTAwMWLMtDHLffFOU54vr/9iVGFcdTAwMTbzOHFt4jR5I1x1MDAwNM9eXHUwMDAxKlx1MDAxMa5cdTAwMGVD3N611i/3jsXJie9/2171/VxuqSmSckx6J4hChL9293/KloVcdTAwMWPzTutcdTAwMTguKYqTfzpcdTAwMTJcdTAwMGLYVpzGJVx1MDAwM7dd3upcdTAwMDTz3rTjVeDSYyQ9Lr17tMBFq90+jmGEik9lKphcdTAwMDS3/viY2fWKN67d3Vx1MDAxOIeC9JNcXDXlZ8JcdTAwMDU7mYJcdTAwMWZcdTAwMGbvXHUwMDFl/lx1MDAwNeEmVVx1MDAxOCJ9 XyXy(0, 0)(0, 0)Widget"},{"location":"guide/input/#mouse-movements","title":"Mouse movements","text":"

    When you move the mouse cursor over a widget it will receive MouseMove events which contain the coordinate of the mouse and information about what modifier keys (Ctrl, Shift etc) are held down.

    The following example shows mouse movements being used to attach a widget to the mouse cursor.

    mouse01.pymouse01.tcss mouse01.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog, Static\n\n\nclass Ball(Static):\n    pass\n\n\nclass MouseApp(App):\n    CSS_PATH = \"mouse01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n        yield Ball(\"Textual\")\n\n    def on_mouse_move(self, event: events.MouseMove) -> None:\n        self.screen.query_one(RichLog).write(event)\n        self.query_one(Ball).offset = event.screen_offset - (8, 2)\n\n\nif __name__ == \"__main__\":\n    app = MouseApp()\n    app.run()\n
    mouse01.tcss
    Screen {\n    layers: log ball;\n}\n\nRichLog {\n    layer: log;\n}\n\nBall {\n    layer: ball;\n    width: auto;\n    height: 1;\n    background: $secondary;\n    border: tall $secondary;\n    color: $background;\n    box-sizing: content-box;\n    text-style: bold;\n    padding: 0 4;\n}\n

    If you run mouse01.py you should find that it logs the mouse move event, and keeps a widget pinned directly under the cursor.

    The on_mouse_move handler sets the offset style of the ball (a rectangular one) to match the mouse coordinates.

    "},{"location":"guide/input/#mouse-capture","title":"Mouse capture","text":"

    In the mouse01.py example there was a call to capture_mouse() in the mount handler. Textual will send mouse move events to the widget directly under the cursor. You can tell Textual to send all mouse events to a widget regardless of the position of the mouse cursor by calling capture_mouse.

    Call release_mouse to restore the default behavior.

    Warning

    If you capture the mouse, be aware you might get negative mouse coordinates if the cursor is to the left of the widget.

    Textual will send a MouseCapture event when the mouse is captured, and a MouseRelease event when it is released.

    "},{"location":"guide/input/#enter-and-leave-events","title":"Enter and Leave events","text":"

    Textual will send a Enter event to a widget when the mouse cursor first moves over it, and a Leave event when the cursor moves off a widget.

    "},{"location":"guide/input/#click-events","title":"Click events","text":"

    There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a MouseDown event, followed by MouseUp when the button is released. Textual then sends a final Click event.

    If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states.

    "},{"location":"guide/input/#scroll-events","title":"Scroll events","text":"

    Most mice have a scroll wheel which you can use to scroll the window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle MouseScrollDown and MouseScrollUp if you want build your own scrolling functionality.

    Information

    Terminal emulators will typically convert trackpad gestures in to scroll events.

    "},{"location":"guide/layout/","title":"Layout","text":"

    In Textual, the layout defines how widgets will be arranged (or laid out) inside a container. Textual supports a number of layouts which can be set either via a widget's styles object or via CSS. Layouts can be used for both high-level positioning of widgets on screen, and for positioning of nested widgets.

    "},{"location":"guide/layout/#vertical","title":"Vertical","text":"

    The vertical layout arranges child widgets vertically, from top to bottom.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2ZW2/aSFx1MDAxNIDf8ytcIvrauHO/VFqtXHUwMDAyTdrck9KkTVdVNLFcdTAwMDdwMLbXXHUwMDFlXHUwMDEyoOp/37FJMVx1MDAxOFx1MDAxY1FcdTAwMWFRtrt+MPjM7XjmO2fOXHUwMDE5f93a3q6ZYaxrr7dreuCqwPdcdTAwMTL1UHuZye91kvpRaItQ/pxG/cTNa3aMidPXr171VNLVJlx1MDAwZZSrnXs/7asgNX3Pj1x1MDAxYzfqvfKN7qV/ZvdT1dN/xFHPM4lTXGayoz3fRMl4LFx1MDAxZOieXHUwMDBlTWp7/8s+b29/ze9T2iXaNSpsXHUwMDA3Om+QXHUwMDE3XHUwMDE1XG5CLsrS0yjMlYVcdTAwMDJRxFx1MDAxMGZsUsNP39jxjPZsccvqrIuSTFS7rNdcdTAwMGbj+/6RSNBdfFF/84lcdTAwMGZIUlxm2/KDoGmGQa6Wm0RputNRxu1cdTAwMTQ1UpNEXf3R90wn06Akn7RNIztcdTAwMTNFqyTqtzuhTtOZNlGsXFzfXGYzXHUwMDE5XHUwMDAwXHUwMDEz6XgmXm9cdTAwMTeSQbZOQjqSUFxmXHUwMDExncjzloI6XHUwMDE4gVx1MDAxOflYl0ZcdTAwMTTYJbC6vFx1MDAwMPlVaHOr3G7bqlx1MDAxNHqTOiZRYVx1MDAxYavELlRR7+HxLYlkXHUwMDBl5kJcdTAwMDI2NUhH++2OsaVcdTAwMThcdEdcdTAwMTDMp8bX+fxD24ZcdTAwMGLBWVGSjVx1MDAxYVx1MDAxZng5XHUwMDBiX8pz11FJ/DhHtTR7mNI4U3ZvXG6konE/9tR4vSFjXGJJiVx1MDAwMVx1MDAxN6yYvMBcdTAwMGa7tjDsXHUwMDA3QSGL3G6BSC799nJcdTAwMTU2KapiU2AkhSRkeTRcdTAwMTk4XCJcdTAwMThGh+qm8/Gi07jY8y/gTVx1MDAwNZolvGahROuDUlx1MDAwModIXHUwMDA0XHUwMDA1L0NJXHUwMDFjgGd5eX4oiVNBJGJcdTAwMGVEXHUwMDEwyFx1MDAwNUxcIkgpwFx1MDAxOK5cdTAwMTFJXGZcdTAwMDCUjHD0XFxI6iDw43QxkKjSWVxujJl1XHUwMDE0kixccuS+PLl517x6f/k+uvp09Fx1MDAwZXXdYd1bXHUwMDA1yPV5SVxmoFx1MDAwMyBlZSdpWSmJV8DxRUtRu+HMo1xikYMgmfWBXHUwMDEzXHUwMDE4IXRKbvu7e+SIQYq5/Fx1MDAxN3vHp1BcdTAwMTSVvpFbs4VcdTAwMThRsDSKny/SgTm6eXu81/Aurlx1MDAxM91NSXC04ShcIupQXHUwMDA2KGFz3lFcIuu5XHUwMDEwnt0zV+LxXHUwMDE2XHUwMDAw+lxcPFwiwFx0XHUwMDAzkEn8e1x1MDAwMmnjxCogXHQj2DpqKZZcdTAwMDbyslx1MDAxZTY/XHUwMDFm3lx1MDAwNVx1MDAwZnz0rn/gN+7vXFx8sOFAUuhAYO9cdTAwMGLcI3IwXHUwMDEwVP4skFx1MDAxMN1cbsGeXHUwMDBiSMKJpIKJ3zZ8xE+kNvaShEO8PJLnXHUwMDFmklH3nDY8vvu3fHOTXHUwMDA0QI3OKpDsKLfTT/RcdTAwMDZAXHSBXHUwMDAz5YJcdTAwMTDSukeHlZBZfc+mXHUwMDBivCQhwsk8nlxcSCW1Sc04r1wiMrtcdTAwMDQr44lcdTAwMTBEmGNcdTAwMDLXiifN7Eg8XHUwMDE3nkZcdTAwMGbMQl9Z6SohQ4xhgsDyiVxybV2NTsO3g+udYDS63mVR86ZcdTAwMTFvOpg2S5jlkZKf4fDJVIaRef5cdTAwMTbEi1x1MDAxNkKbSeBcco9cdTAwMTeLdY1C0/RHOlx1MDAwZi1mpPuq51x1MDAwN8OZpclBtJrahW5rMz2VqbZjjk97ZmrvXHUwMDA2fjtDtVx1MDAxNujWLMPGd1UwKTbR1Ju7dnRlu0tcdTAwMGW88ltEid/2Q1x1MDAxNXyY1WR1705cdTAwMTCv9u6SXHUwMDAwIaRcXD5cdTAwMDKW51x1MDAxMFx1MDAxY1x1MDAwZY7htZa3rZM9fkz76d6mXHUwMDFiXHUwMDExXHUwMDA20mFcdTAwMDLPXHUwMDA2XHUwMDE2w9ztXHUwMDEzR1xiJH7y1OpcdTAwMDVcdTAwMDEuoJwtXGI5XGIlXHUwMDBlp7jihFx1MDAwMELqXGLISFaaXHUwMDBmXHUwMDAz56yNXCJJKON0vdFcdTAwMDdcdTAwMDXUboZriT4oJlV8YmRcdTAwMTM0xvnyeKrPO83hzd6nncuTw+O6XHUwMDFj+Gr/Q3Pj8bTBXHUwMDA3gVxczmVoWWhApSzFXHUwMDA2K1x1MDAwMeoy3aKLXHUwMDAxtUFxJaCEOyjXazzIPJ9cYoBcZm+I15utUWhDtWfjUyVJ9LD4XHUwMDFjqzpX43ZcdTAwMGKU8Fx1MDAwN+KP+5OUjc5cdTAwMGUur0x8XHUwMDE2JOfDs1x1MDAwN4YvVmNzfUerwsa/slx1MDAxY1x1MDAwMH8/yypHrWUybVx1MDAxYaawfprMqlxcXHI4nHO8OFXDXGI5XHUwMDA0US5cdTAwMTZcdTAwMWZnXHRcblxiWyFcdTAwMWPONVt3eJJcdTAwMWGVmLpcdTAwMWZ6ftguN9GhV1FcdTAwMTKo1DSiXs83Vo3zyFx1MDAwZk25Rt7vblx1MDAwNnZHq7kow/Y8XVa2gDjrsfhSll3Fv+1cdTAwMDKR/GHy/8vLxbXnVjK7ptew6GFr+vdHs1x1MDAwNVwiy8JJoGMxlVhQtLy1plx1MDAwN/qwvdc+xfuXcrA/knetNiSbvpNYN+0ghNjc0Vxutnnk/LeIX5M/XHUwMDAwTG3ehjb9POW/lEBUWZR84ps3QcKGrj/wzftYJo2O2b3bXHUwMDFk6uOr3TPVc+/ev918i+JcdTAwMGVcdTAwMTFg/vScWIviXGbymVx1MDAxM6NfZFFYXHUwMDEwXHS53aP/t6i1W9TW475XU3HcNHaGbI2xfdlF8L3H1yz6q937+qG+6IQwv7JecyvN7EFnS/D129a3f1x1MDAwMLFE1Vx1MDAwMCJ9 WidgetWidgetWidget

    The example below demonstrates how children are arranged inside a container with the vertical layout.

    Outputvertical_layout.pyvertical_layout.tcss

    VerticalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass VerticalLayoutExample(App):\n    CSS_PATH = \"vertical_layout.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = VerticalLayoutExample()\n    app.run()\n
    Screen {\n    layout: vertical;\n}\n\n.box {\n    height: 1fr;\n    border: solid green;\n}\n

    Notice that the first widget yielded from the compose method appears at the top of the display, the second widget appears below it, and so on. Inside vertical_layout.tcss, we've assigned layout: vertical to Screen. Screen is the parent container of the widgets yielded from the App.compose method, and can be thought of as the terminal window itself.

    Note

    The layout: vertical CSS isn't strictly necessary in this case, since Screens use a vertical layout by default.

    We've assigned each child .box a height of 1fr, which ensures they're each allocated an equal portion of the available height.

    You might also have noticed that the child widgets are the same width as the screen, despite nothing in our CSS file suggesting this. This is because widgets expand to the width of their parent container (in this case, the Screen).

    Just like other styles, layout can be adjusted at runtime by modifying the styles of a Widget instance:

    widget.styles.layout = \"vertical\"\n

    Using fr units guarantees that the children fill the available height of the parent. However, if the total height of the children exceeds the available space, then Textual will automatically add a scrollbar to the parent Screen.

    Note

    A scrollbar is added automatically because Screen contains the declaration overflow-y: auto;.

    For example, if we swap out height: 1fr; for height: 10; in the example above, the child widgets become a fixed height of 10, and a scrollbar appears (assuming our terminal window is sufficiently small):

    VerticalLayoutScrolledExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2582\u2582 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502

    With the parent container in focus, we can use our mouse wheel, trackpad, or keyboard to scroll it.

    "},{"location":"guide/layout/#horizontal","title":"Horizontal","text":"

    The horizontal layout arranges child widgets horizontally, from left to right.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aa0/bSFx1MDAxNIa/8ytQ+rVM536ptFpcdTAwMTEuLTQtlNAtdFVVjj1JZnFsYztcdGnFf9+xQ+PE2GxcYlHKatdCSTzX45nnXHUwMDFjvzPDj63t7UY6iXTj9XZD37iOb7zYXHUwMDE5N15m6SNcdTAwMWQnJlxmbFx1MDAxNs7vk3BcdTAwMTi7ecl+mkbJ61evXHUwMDA2Tnyl08h3XFxcckYmXHUwMDE5On6SXHUwMDBlPVx1MDAxM1x1MDAwMjdcdTAwMWO8MqlcdTAwMWUkv2efXHUwMDFmnIH+LVxuXHUwMDA3Xlx1MDAxYYOik1x1MDAxZO2ZNIynfWlfXHUwMDBmdJAmtvU/7f329o/8c866WLupXHUwMDEz9HydV8izXG5cdTAwMDNcdTAwMTFH5dRcdTAwMGZhkFx1MDAxYitcdTAwMDRnTFBOZ1x1MDAwNUyyb7tLtWdzu9ZkXeRkSY1wrHY6o1x1MDAxMbuiTv8vfja8uWw3o6LXrvH9djrxc6vcOEySnb6Tuv2iRJLG4ZX+bLy0n9lWSp/VTUI7XHUwMDEwRa04XHUwMDFj9vqBTpKFOmHkuCadZGlcdTAwMTDOUqdcdTAwMDPxertIubF3XHUwMDFjXHUwMDAxXHUwMDA0XHUwMDEx45jNkvOKXHUwMDA0XHUwMDAyXHUwMDBlXHUwMDA1xUhcblYyZi/07Vx1MDAxNFhjXsD8KszpOO5Vz9pcdTAwMTR4szJp7Fx1MDAwNEnkxHaiinLju8ekilx1MDAwM1wipILz3fe16fVTm0uwXHUwMDA0kpL5/nU+XHUwMDAxXGJhXHUwMDAyle2ZzHKyXqMjL2fha3nw+k5cdTAwMWPdXHJSI8lu5izOjD2YXHUwMDAzqag8jDxnOuGIc4yVXCJcbjNajJ5vgiubXHUwMDE5XGZ9v0hcdTAwMGLdq4KRPPX25SpsXHUwMDEyXsemopxcdTAwMGKO4fJs9odB29//LryTZvzHXHUwMDA1jpLjT+ZdXHKbJb5cdTAwMTapxJukktPFuc8rYlx1MDAwNVx1MDAwNJSqRMXaqaSgXHUwMDA2ScxcdTAwMDHCXGKqKii55Vx1MDAxMVNcdTAwMDHR5qAkXHUwMDEwYiigQOuCUvu+iZJqJFx1MDAxMalDkiNCqP1cdTAwMTNLI/lpsPd2ctChqOd8eXt4zVvHcFx1MDAxZq2C5OZcdTAwMDKlwFx1MDAwMImFaDiNk1xuMIZcdTAwMDV5KpEvulx1MDAwZcNcZt+nXHUwMDExYYBRyVx1MDAxN2Y8XCJcdTAwMDQoI1xi36NcdTAwMTFbw1xiUYJvNERaKyGkXHUwMDFioZGLOlx1MDAxYe1gXHREkIBqaVx1MDAxY9E5vWmpTtg62lx1MDAxOUYnh63v3eB493njaN+cwlx1MDAwZYLi94mUgFx1MDAxMlXGYiVcIjtcdTAwMTCytVx1MDAxMVx0MWFIWiY3TyTeXHUwMDAwkVx1MDAxONfKSeuLUmJCKVmayPeuuL5pXlx1MDAxY6TiYHzcla0v+/1o/3lcdTAwMTOJsOWC2ZBTISaFXHUwMDE1ckzAJyOJcEdKvi4kXHUwMDExQlx1MDAxMCumXHUwMDE4+1x1MDAxNyP5oI7k9WtcdTAwMWOroZVgXG7hpZmM3r9PzpvDvdZxcn74RSr+5qzztobJvuP2h7H+9VRcblx1MDAwMaxywVx1MDAxMpWZXHUwMDE0XGYwWKZ19Vx1MDAxNzevopJZuShcdFOVWGIuXHUwMDAxrMKSWIVPIedqk1QqSlx1MDAxMCNiXVSm+iatVpGyXHUwMDE2SIVskGRKLS8jj1x1MDAwZttJ2mpCdlx1MDAxMnjJ8FrH74KLi+dO5DROksVcdTAwMTVGVlx1MDAxNUtcYlxixvjJSD64upnb1ChQrFjNWFxirYhcIlx1MDAxYlxczWShUVwiwsgjICzmOlxm0rb5rnOlsZB66FxmjD9ZmK6cTmupnfyeTufHMtG2z5xGuVB61ze9jN+Gr7uLYKfGdfxZdlx1MDAxYc49uWt7d2xz8ZFXfoowNj1cdTAwMTM4/vmiJatHeilonWNcdFx1MDAxYuelJIIv7Ve4NWiN+67eXHUwMDFm6dbHyWRMh2ejo+fuV5gxUN5cdTAwMWGYRnr7XG6AVns+OdJP1UdlpIdcdTAwMWPYt7pYeM3MRXpcdTAwMDZ4aZPtp59Jqz6gIHijXHUwMDEyRFFcdTAwMWJq4WP8bHUwXHUwMDE1YnVgXCJMoWBYLs3ldde9hEcn15+Cvve5N1x1MDAxYZs2Pfz23Lkkklx1MDAwMJS90O8pXHUwMDEwXHUwMDA0eFmarIIlxrKjq7HkXHUwMDE4SJ53QVV2iSo4XHUwMDE1kFx1MDAwNFcqXHUwMDExpCRWVJBccitcdTAwMTEsXHUwMDA15OuCs1aJqHouKYNcdTAwMWOJR+xnnYs3x6NT7/SjY051e6/3wfF26/aznlxymFhIIGBpVTZcdTAwMTVcIlx1MDAxMqh1bCGsQYhQqlxis4vHTetcdTAwMTAqXHUwMDFmJYb/Szqk1qPkXHUwMDAzXHUwMDEyhNs4w+dm8Vx1MDAxZlx1MDAwZtQ+XHUwMDFmOJff6NklvDa9sVaBOPlr8tw9imJcdTAwMDHKbvPToaDgpf3jX6TsieJ2sUfxpj0qO7T636Oyr3tcdTAwMWXlxHE4rnQpWOtSdrVoXHUwMDAzOH/EnuK3uHnK946uXHUwMDA35uDj8I1vlHmz667mUlx1MDAxYjxcdJSA4vu73FxmXCLwkCtJ0WWdp1x1MDAxY1x1MDAwMTJA+KK7XHUwMDE2+4mAK1U6XHUwMDE4v/MtaHOs4EUr7HHn1q3mW1xmMkVcdTAwMWVz6jJnh1x1MDAxM6dNXHUwMDEzeCbolavowKvJ8Z0k3Vx1MDAwYlx1MDAwN1x1MDAwM5NaM05DXHUwMDEzpOVcdTAwMTJ5u7tcdTAwMTnVfe3cc1x1MDAxMdvyfF5cdTAwMTn/KGux+K+O7Cp+bVx1MDAxN3zkN7PfX19Wlq6Yyewq5rBoYGv++3brrsmGXHUwMDEzRe3UXHUwMDBluDVo6rh2To13XHUwMDE3kYrnaoyMXHUwMDFlN6v2XHUwMDA38ytcdTAwMGJcdTAwMDC5+2d+prOn+3G7dfs38GbaXHUwMDA3In0= WidgetWidgetWidget

    The example below shows how we can arrange widgets horizontally, with minimal changes to the vertical layout example above.

    Outputhorizontal_layout.pyhorizontal_layout.tcss

    HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass HorizontalLayoutExample(App):\n    CSS_PATH = \"horizontal_layout.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalLayoutExample()\n    app.run()\n
    Screen {\n    layout: horizontal;\n}\n\n.box {\n    height: 100%;\n    width: 1fr;\n    border: solid green;\n}\n

    We've changed the layout to horizontal inside our CSS file. As a result, the widgets are now arranged from left to right instead of top to bottom.

    We also adjusted the height of the child .box widgets to 100%. As mentioned earlier, widgets expand to fill the width of their parent container. They do not, however, expand to fill the container's height. Thus, we need explicitly assign height: 100% to achieve this.

    A consequence of this \"horizontal growth\" behaviour is that if we remove the width restriction from the above example (by deleting width: 1fr;), each child widget will grow to fit the width of the screen, and only the first widget will be visible. The other two widgets in our layout are offscreen, to the right-hand side of the screen. In the case of horizontal layout, Textual will not automatically add a scrollbar.

    To enable horizontal scrolling, we can use the overflow-x: auto; declaration:

    Outputhorizontal_layout_overflow.pyhorizontal_layout_overflow.tcss

    HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass HorizontalLayoutExample(App):\n    CSS_PATH = \"horizontal_layout_overflow.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalLayoutExample()\n    app.run()\n
    Screen {\n    layout: horizontal;\n    overflow-x: auto;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    With overflow-x: auto;, Textual automatically adds a horizontal scrollbar since the width of the children exceeds the available horizontal space in the parent container.

    "},{"location":"guide/layout/#utility-containers","title":"Utility containers","text":"

    Textual comes with several \"container\" widgets. Among them, we have Vertical, Horizontal, and Grid which have the corresponding layout.

    The example below shows how we can combine these containers to create a simple 2x2 grid. Inside a single Horizontal container, we place two Vertical containers. In other words, we have a single row containing two columns.

    Outpututility_containers.pyutility_containers.tcss

    UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Vertical(\n                Static(\"One\"),\n                Static(\"Two\"),\n                classes=\"column\",\n            ),\n            Vertical(\n                Static(\"Three\"),\n                Static(\"Four\"),\n                classes=\"column\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n
    Static {\n    content-align: center middle;\n    background: crimson;\n    border: solid darkred;\n    height: 1fr;\n}\n\n.column {\n    width: 1fr;\n}\n

    You may be tempted to use many levels of nested utility containers in order to build advanced, grid-like layouts. However, Textual comes with a more powerful mechanism for achieving this known as grid layout, which we'll discuss below.

    "},{"location":"guide/layout/#composing-with-context-managers","title":"Composing with context managers","text":"

    In the previous section, we've shown how you add children to a container (such as Horizontal and Vertical) using positional arguments. It's fine to do it this way, but Textual offers a simplified syntax using context managers, which is generally easier to write and edit.

    When composing a widget, you can introduce a container using Python's with statement. Any widgets yielded within that block are added as a child of the container.

    Let's update the utility containers example to use the context manager approach.

    utility_containers_using_with.pyutility_containers.pyutility_containers.tcssOutput

    Note

    This code uses context managers to compose widgets.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            with Vertical(classes=\"column\"):\n                yield Static(\"One\")\n                yield Static(\"Two\")\n            with Vertical(classes=\"column\"):\n                yield Static(\"Three\")\n                yield Static(\"Four\")\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n

    Note

    This is the original code using positional arguments.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Vertical(\n                Static(\"One\"),\n                Static(\"Two\"),\n                classes=\"column\",\n            ),\n            Vertical(\n                Static(\"Three\"),\n                Static(\"Four\"),\n                classes=\"column\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n
    Static {\n    content-align: center middle;\n    background: crimson;\n    border: solid darkred;\n    height: 1fr;\n}\n\n.column {\n    width: 1fr;\n}\n

    UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    Note how the end result is the same, but the code with context managers is a little easier to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!

    "},{"location":"guide/layout/#grid","title":"Grid","text":"

    The grid layout arranges widgets within a grid. Widgets can span multiple rows and columns to create complex layouts. The diagram below hints at what can be achieved using layout: grid.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW3PaRlx1MDAxNMff8yk85DVsds/eM9PpONimwZc40NZ2O52MkFx1MDAxNiQjkCxcdOM4k+/elUhcdTAwMTFcYpNQyrhcdTAwMDQ9aEZnb8e7P875n/XnXHUwMDE3XHUwMDA3XHUwMDA3texTbGpvXHUwMDBlaubBdcLAS5xJ7VVuvzdJXHUwMDFhRCPbXHUwMDA0xXdcdTAwMWGNXHUwMDEzt+jpZ1mcvnn9eugkXHUwMDAzk8Wh41x1MDAxYXRcdTAwMWakYydMs7FcdTAwMTdEyI2Gr4PMXGbTn/P3hTM0P8XR0MtcdTAwMTJULlI3XpBFyXQtXHUwMDEzmqFcdTAwMTllqZ39T/t9cPC5eM95l1x1MDAxODdzRv3QXHUwMDE0XHUwMDAziqbSQVx1MDAwZbRqvYhGhbNSYYEpV3rWIUiP7HKZ8Wxrz7psypbcVLtpnVx1MDAxZl596NavLlx1MDAxZlx1MDAxZVx1MDAxYVx1MDAxZlx1MDAwNmN8d/rQKlftXHUwMDA1YdjJPoWFV25cdTAwMTKlad13Mtcve6RZXHUwMDEyXHLMVeBlvu1DKvbZ2DSyXHUwMDFiUY5KonHfXHUwMDFmmTRdXHUwMDE4XHUwMDEzxY5cdTAwMWJkn3JcdTAwMWLGM+t0I95cdTAwMWOUloeiXHUwMDA3Q1RSzISSfNZSjFx1MDAxNVxuXHUwMDExxjglwCvuNKLQXHUwMDFlgnXnJS6e0qGu41x1MDAwZfrWq5E365MlziiNncRcdTAwMWVV2W/y9Vx1MDAwZmVa2OWVxmJuXHUwMDEx31x1MDAwNH0/s61cdTAwMTRcdTAwMTRSjM45lpriXGKIXHUwMDEyUlGQsjzCfNX4nVfQ8Fd1+3wnib9uUy3NP+Y8zp09nkOpXHUwMDFjPI49Z3rkRFxioFhcYo0xLfcvXGZGXHUwMDAz2zhcdTAwMWGHYWmL3EFJSWH98mpcdTAwMDM6iYaVdFxuKblQhKxN51x1MDAxOVHtWH7sezeP/lHnulx1MDAwMVx1MDAwZbm+XUFnhbBFLuFZuVx1MDAwNFxuXGbIMpdcdTAwMTIxXCL1XHUwMDAysNvnkqFcdTAwMTVQgkBcdTAwMDRcYtZPYSmVXHUwMDEyhCtJfmAsTVx1MDAxOFx1MDAwNnH6NJRCroKSgKCSYGB8bSpvvZPTNlx1MDAxN73r5nEr6pCscXtydrZcdJXPXHUwMDE4LVx0Q0phxVx1MDAxN06/XHUwMDE4K1x1MDAwNdJcdTAwMTJcdTAwMDRcdTAwMTf/LVq+7DlcdTAwMWM4LFx1MDAxM0lcdTAwMDBcdTAwMDFhi9FwxiQhqFx1MDAxYainRCrGXGLYUfr5gSTPXHUwMDAwJFx1MDAwMFlccqSywYNpvj6QN2MxOUs8t3nphONcdTAwMTNcdTAwMTI770izu+NAUo1cdTAwMThwqebPfsojR1x1MDAxNUw3o7GLMd9cdTAwMTaNVHPFlFx1MDAwNrmnNM5tRpVGLG101DZGrk2j4dK9ad+178bvo7fHl0o49cbJjtMobNZcXFx1MDAxNpJcdTAwMTbFLVx1MDAwNEZcdTAwMDJdm123hVwiyVx1MDAxMzVVWvxcdTAwMGapemssfltBXHUwMDEyvlJCMlxuQtn6Zv1cdTAwMDKn58i02ei/u3J9OrlodY/Os4+rkrXvuP44MTvAI1x1MDAxMMujkMtMgqVmSV1unq7FU1xcaoxA0pzL6UR0XHUwMDE5T1x0/0hZpotnXHRTomxxROeF195hOldmV6OmxsDtr1x1MDAxNNbnNFx1MDAxOep2M2xcdTAwMWReXHUwMDBl07f3LGh15MVxvOucUmo5kFhiWk3jgDXKz2KxXHUwMDE22S6oXG4jOc8p2YRT4FQrofT+ckphdfGDqS3KQdP1Of2j0W1cdTAwMDa3V4T83nPdX25cdTAwMWb751x1MDAxN0fOrnNq01x1MDAwNlJSaL7MqY2nXFxhvChEt1x1MDAxY1CZLb6YzJXElEK5XGZqrkB4cZc1XVx1MDAwYosqqfZcdTAwMTclOFx1MDAwNjJcdTAwMTf3941UXHUwMDAyeCWpXHUwMDAyXHUwMDEzRYGT9XXo+aVw621y18x4NOanp93eIVx1MDAxOe06qXnmZ5zj5cKIYkBWjW8h9U9cdTAwMDXp06mfXCLNXGLLg/bmqZ8qZXOCwPurUC2H30j9Np3k97xrg0rYUVx1MDAwMHU2oYG+gUFrXHUwMDEyTn5cdTAwMTk+7DqolDKElV4sX6ac2lpKq8rF/HY53U7mXHUwMDE33Go0YPub+blcdTAwMTIrMZVC2OPjZP1bps71yfk5jDJcdTAwMGbjTP7620X9LIuPdlx1MDAxZNOikpJM4KWLT6ol4lpXWjbhXHUwMDE0QHXNk5yCJohcbrlcdTAwMTCx/1xyofm9PFx1MDAxMXR/XHUwMDAzKdPf0Ka5XHUwMDFlgLl7ju9cdTAwMDH6cVx1MDAxON++T4Pe44fw0L+Pzvw2ucG7XHUwMDBlKLNxlC39X2ZcdTAwMDboVqTpakBcdFx1MDAxM0hPr2FXStPvclxuWFx1MDAxMK2A/Vx1MDAxOJf19l1MWnPiuJPZKW3zlFrrdeB1gkezME3tPjCTt09cdP7iqb34yn7Ol8l9/vzlxZe/XHUwMDAxUO5ccsMifQ==

    Note

    Grid layouts in Textual have little in common with browser-based CSS Grid.

    To get started with grid layout, define the number of columns and rows in your grid with the grid-size CSS property and set layout: grid. Widgets are inserted into the \"cells\" of the grid from left-to-right and top-to-bottom order.

    The following example creates a 3 x 2 grid and adds six widgets to it

    Outputgrid_layout1.pygrid_layout1.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout1.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3 2;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    If we were to yield a seventh widget from our compose method, it would not be visible as the grid does not contain enough cells to accommodate it. We can tell Textual to add new rows on demand to fit the number of widgets, by omitting the number of rows from grid-size. The following example creates a grid with three columns, with rows created on demand:

    Outputgrid_layout2.pygrid_layout2.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Seven\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout2.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n        yield Static(\"Seven\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Since we specified that our grid has three columns (grid-size: 3), and we've yielded seven widgets in total, a third row has been created to accommodate the seventh widget.

    Now that we know how to define a simple uniform grid, let's look at how we can customize it to create more complex layouts.

    "},{"location":"guide/layout/#row-and-column-sizes","title":"Row and column sizes","text":"

    You can adjust the width of columns and the height of rows in your grid using the grid-columns and grid-rows properties. These properties can take multiple values, letting you specify dimensions on a column-by-column or row-by-row basis.

    Continuing on from our earlier 3x2 example grid, let's adjust the width of the columns using grid-columns. We'll make the first column take up half of the screen width, with the other two columns sharing the remaining space equally.

    Outputgrid_layout3_row_col_adjust.pygrid_layout3_row_col_adjust.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout3_row_col_adjust.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 2fr 1fr 1fr;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Since our grid-size is 3 (meaning it has three columns), our grid-columns declaration has three space-separated values. Each of these values sets the width of a column. The first value refers to the left-most column, the second value refers to the next column, and so on. In the example above, we've given the left-most column a width of 2fr and the other columns widths of 1fr. As a result, the first column is allocated twice the width of the other columns.

    Similarly, we can adjust the height of a row using grid-rows. In the following example, we use % units to adjust the first row of our grid to 25% height, and the second row to 75% height (while retaining the grid-columns change from above).

    Outputgrid_layout4_row_col_adjust.pygrid_layout4_row_col_adjust.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout4_row_col_adjust.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 2fr 1fr 1fr;\n    grid-rows: 25% 75%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    If you don't specify enough values in a grid-columns or grid-rows declaration, the values you have provided will be \"repeated\". For example, if your grid has four columns (i.e. grid-size: 4;), then grid-columns: 2 4; is equivalent to grid-columns: 2 4 2 4;. If it instead had three columns, then grid-columns: 2 4; would be equivalent to grid-columns: 2 4 2;.

    "},{"location":"guide/layout/#auto-rows-columns","title":"Auto rows / columns","text":"

    The grid-columns and grid-rows rules can both accept a value of \"auto\" in place of any of the dimensions, which tells Textual to calculate an optimal size based on the content.

    Let's modify the previous example to make the first column an auto column.

    Outputgrid_layout_auto.pygrid_layout_auto.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502First\u00a0column\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout_auto.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"First column\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: auto 1fr 1fr;\n    grid-rows: 25% 75%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Notice how the first column is just wide enough to fit the content of each cell. The layout will adjust accordingly if you update the content for any widget in that column.

    "},{"location":"guide/layout/#cell-spans","title":"Cell spans","text":"

    Cells may span multiple rows or columns, to create more interesting grid arrangements.

    To make a single cell span multiple rows or columns in the grid, we need to be able to select it using CSS. To do this, we'll add an ID to the widget inside our compose method so we can set the row-span and column-span properties using CSS.

    Let's add an ID of #two to the second widget yielded from compose, and give it a column-span of 2 to make that widget span two columns. We'll also add a slight tint using tint: magenta 40%; to draw attention to it.

    Outputgrid_layout5_col_span.pygrid_layout5_col_span.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502\u2502Four\u2502\u2502Five\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Six\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout5_col_span.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two [b](column-span: 2)\", classes=\"box\", id=\"two\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n#two {\n    column-span: 2;\n    tint: magenta 40%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Notice that the widget expands to fill columns to the right of its original position. Since #two now spans two cells instead of one, all widgets that follow it are shifted along one cell in the grid to accommodate. As a result, the final widget wraps on to a new row at the bottom of the grid.

    Note

    In the example above, setting the column-span of #two to be 3 (instead of 2) would have the same effect, since there are only 2 columns available (including #two's original column).

    We can similarly adjust the row-span of a cell to have it span multiple rows. This can be used in conjunction with column-span, meaning one cell may span multiple rows and columns. The example below shows row-span in action. We again target widget #two in our CSS, and add a row-span: 2; declaration to it.

    Outputgrid_layout6_row_span.pygrid_layout6_row_span.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02\u00a0and\u00a0row-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502\u2502 \u2502Three\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout6_row_span.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two [b](column-span: 2 and row-span: 2)\", classes=\"box\", id=\"two\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\napp = GridLayoutExample()\nif __name__ == \"__main__\":\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n#two {\n    column-span: 2;\n    row-span: 2;\n    tint: magenta 40%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Widget #two now spans two columns and two rows, covering a total of four cells. Notice how the other cells are moved to accommodate this change. The widget that previously occupied a single cell now occupies four cells, thus displacing three cells to a new row.

    "},{"location":"guide/layout/#gutter","title":"Gutter","text":"

    The spacing between cells in the grid can be adjusted using the grid-gutter CSS property. By default, cells have no gutter, meaning their edges touch each other. Gutter is applied across every cell in the grid, so grid-gutter must be used on a widget with layout: grid (not on a child/cell widget).

    To illustrate gutter let's set our Screen background color to lightgreen, and the background color of the widgets we yield to darkmagenta. Now if we add grid-gutter: 1; to our grid, one cell of spacing appears between the cells and reveals the light green background of the Screen.

    Outputgrid_layout7_gutter.pygrid_layout7_gutter.tcss

    GridLayoutExample OneTwoThree FourFiveSix

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout7_gutter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-gutter: 1;\n    background: lightgreen;\n}\n\n.box {\n    background: darkmagenta;\n    height: 100%;\n}\n

    Notice that gutter only applies between the cells in a grid, pushing them away from each other. It doesn't add any spacing between cells and the edges of the parent container.

    Tip

    You can also supply two values to the grid-gutter property to set vertical and horizontal gutters respectively. Since terminal cells are typically two times taller than they are wide, it's common to set the horizontal gutter equal to double the vertical gutter (e.g. grid-gutter: 1 2;) in order to achieve visually consistent spacing around grid cells.

    "},{"location":"guide/layout/#docking","title":"Docking","text":"

    Widgets may be docked. Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container. Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2aWXPbNlx1MDAxMIDf8ys0ymvM4D4y0+nItpKocWwnjuOj0+nQJCTSokiWpCxLXHUwMDE5//cuaVXURVx1MDAxZrKqqtGDLWFcdGBcdHy72Fx1MDAwNfDjVa1Wz4axqb+r1c2tY1x1MDAwN76b2IP6m7z8xiSpXHUwMDFmhSBcIsXvNOonTvGkl2Vx+u7t256ddE1cdTAwMTZcdTAwMDe2Y6xcdTAwMWI/7dtBmvVdP7KcqPfWz0wv/TX/e2j3zC9x1HOzxCo72TGun0XJfV8mMD1cdTAwMTNmKbT+O/yu1X5cdTAwMTR/p7RLjJPZYScwRYVCVCrIXHSbLz2MwkJZzDXHTCuMJk/46T70l1x1MDAxOVx1MDAxN8Rt0NmUkryovk9cdTAwMWKNLDiPm7vvveD4YtQ6b3mdstu2XHUwMDFmXHUwMDA0J9kwKNRykihNdzw7c7zyiTRLoq45893MyzWYK5/UTSNcdTAwMTiJslZcdTAwMTL1O15o0nSmTlx1MDAxNNuOn1xy8zJUvsL9SLyrlSW3+TBcYksppLimkk9cdTAwMDR5VVwiLc0wQ4LwOWX2olx1MDAwMOZcdTAwMDCUeY2KT6nOle10O6BT6E6eyVx1MDAxMjtMYzuBmSqfXHUwMDFijF+TaWFRqfRMJ57xO15cdTAwMDZSSpSl2LReqSkmQFLMXHUwMDE4RVxcTVx1MDAwNHmnccstWPhjfuw8O4nHY1RP81x1MDAxZlNcbue6NqdAKiv3Y9e+n28sXHUwMDA0YVx1MDAxYyOtJKJcdTAwMTN54IddXHUwMDEwhv0gKMtcIqdbXCJSlN69WYFNrGklm4wjXCI1k09nM+yfXHUwMDFj73qX3/rdXHUwMDEz3jvY/Vx1MDAxNCFf7lWwOcfXLJVkk1RKhlxiZUuoJJjNUbF2KplVgSRcdTAwMTFcdTAwMTYmQMIyKJHGXHUwMDFhXHUwMDFjXHUwMDA3+lx1MDAxZkNpgsCP0+VIXG5VhaQg4D84werJRLZUvG9uj6L9a9p2/mzEPlx1MDAxYuhwXHUwMDE1XCI35yeFtKhSQmI1RyRcdTAwMDVUheZcdTAwMTS/zE++btuccLJIIyaLxE94xNhic13f08hcdTAwMDVSQlx1MDAwYkV+Tlx1MDAxYVx0IVU0YiykJuA7no6jz0dnXel+uP14ddL63CW94Pw82G5cdTAwMWM1tjSli6s25ZbgL1xcslx1MDAwMcUrhPi6UCyCXGLNlPhJUZSiXG5FmFx1MDAxZKQwsPhkXHUwMDEym8R8O/R6XzzxvXf05f1+s5mNXHUwMDBltptEjKXF5Cx0Y1x1MDAxMl9cdTAwMWE7vsbkXG587rpA1FgqJqT8P6/QXHUwMDBmh41cdTAwMTC5VLpFSlx1MDAxOVx1MDAxM1x1MDAxMOc/PW68uTj8XHUwMDFj68ug2T/5yptcdTAwMTk7v4pdv1x1MDAwMkbPdrx+Yv57XHUwMDFjuVx1MDAwNjTIXFxKkVfldJ3rtFhGJdWWXHUwMDEwRcZ031x1MDAxMF2Ek1wiYtFZ5cZ0XG4miCZywys2jFx1MDAwNkViM3TKXHUwMDA3Mm4qXGJoJJ+R1Vx1MDAxY1x1MDAxZYlcdTAwMTZpXFz3XHUwMDAzX35cdTAwMTmq9vfWzaCRbjudXHUwMDA0c0stYJjXXHUwMDE11FKQ2NGXZjZjn7mMT8iYLYZcdTAwMTHmS1NcdTAwMWJJQCghZJRMXHUwMDE3n3lAMVx1MDAwNF2SK0o2SygkXHUwMDE3SMrNXHUwMDEwqpSsXCKU5IqoZ1x1MDAxMbpz9MGcfo/OzcDP1M7p8V+O23K2n1BlgUdcdTAwMTBswYFiziyBJCMzKdB6XHUwMDExJVpZfFVAXHUwMDA1olQwyjaLJ5jFxvBElVmPRlhTNZ1cdTAwMTY9Rif969PnVn/onO3tfrn0mmfNTvL5z+2nXHUwMDEzsu2lyzthzFKU6lx1MDAxN29ccj1Cp0RcdTAwMTDN4dVcdTAwMWMoQ+A/MaGbXHJAYc2hXHUwMDAyb4RQgmTlLlx1MDAxMURHVEJcdTAwMWH4jGyo8enyMOy6UUz6R3vu7c23bD+Mt1x1MDAxZFFOmSU5X7KjLoWl+Dr8JyHqyiwlXHUwMDE0wotcIrZcdTAwMThcdTAwMTOotVjkXHUwMDE0XHUwMDBibTGxdFx1MDAwZlx1MDAxM1x1MDAxMiRcblx1MDAxM1x1MDAwNPRveIlcdTAwMTeYboZQrFHl1rpSmmBKpnKox1x1MDAwMFx1MDAxNa33e8e/XHRcdTAwMTnstM2+STJFXHUwMDBm+61tXHUwMDA3NF/hXHRcdTAwMTWUqPlcdTAwMTiUQs4uOV5DkvSQXHUwMDBmXHUwMDA1K1x1MDAxOJ83XHUwMDE1XHItI1RbsJapXHUwMDEyYzpcdTAwMGYqpVx1MDAxNFDlerMnQDBykESvi1M7SaLBUi/KKlx1MDAxMWWUS4LwM1x1MDAwZSbbl1xyXHUwMDE571x1MDAwN5/E6FwiuKTy4243XHUwMDFjXHUwMDBlVkN0c8c/WECaRDBmRFx1MDAxMIWEwnKGU5afTT5cZil3NEPuqpDmm1xiXHUwMDEwXHUwMDAyc5jyonu2yChB2lr0n1x1MDAwMoPqXG6vcjJZKLdcdTAwMWGXlMKQPsd/TulhJ9muXHUwMDFmun7YXHUwMDAx4T+M1ian661cdTAwMDKig49fvUBcdTAwMGVcdGmedk4v5ECnX9HpRNdcdTAwMWOjyOnnWu4gXHUwMDBiwlxywlx1MDAxNbhcdTAwMTdCNSyDik891rHjYv4teZ/ojiV3XHUwMDEzfUzoltrMvoCdZntRr+dn8OrHkVx1MDAxZmbzT1x1MDAxNO/SyI3KM7Y7L4WWp2Xz1lx1MDAxN+ctlldcdPJP+a1W4ln8mHz/483Sp3cq+SmkXHUwMDA1OmVcdTAwMWKvpv9XOYvM3GZLfVx1MDAwNa5cZrgkg0BcdTAwMTRcdTAwMDLO0sE+5itcdTAwMWWe5i1dznCxnC1cdTAwMWNcdTAwMTQzXG6xOnn5WUi1k4BwfolbWPBcdFx1MDAxME9cdIhcdP+N449cdEM/pvB6kt+fYeveXHUwMDE0JpK7f4D8N1x1MDAxY047XG6zXHUwMDEzf1TsqKCZ0vd2z1x1MDAwZoYzXHUwMDE4XHUwMDE00OeXa4q2ajDwXHUwMDFkk01PV2qg61wiuVAzlVx1MDAxYYHfya2jXHUwMDFlmPas2WS+Y1x1MDAwN1x1MDAxM3FcdTAwMTZNXHKvXHUwMDAzStjQXFzSWnBcdTAwMWVR4nf80Fx1MDAwZb4tVWglw8XVW02QIVxuSVx1MDAxOFJPz5RcdTAwMGVcdTAwMDJzdINij35cdTAwMThdOFe/XVx1MDAwZkbBaMWt+s2t8lRLaz6NXHUwMDA3O7aUnj+/WfNcdTAwMDVcdTAwMGbMS4VcdTAwMWawXFyMXHUwMDE01lx1MDAxMHuwXHKb7uHxdYNeKXamlZdcXCeDXHUwMDBmo1x1MDAwMTtYm+lyjMRz9qteZrpcdTAwMDf2MOpnY0tJt8F25zRaMUTn1Vx1MDAxN7RcdTAwMTCh+DnXs1x1MDAxZZ7uLbVdxoVFXHUwMDE5yq/hIaXx1GWL+1x1MDAwMJ1Z+rG7g0q2+dXqRqykhbjGeqzA1MZTadJcdTAwMTBcdTAwMWFUXdeCcsGJ5qusyy9cdNVz81OrmN9TQ/VcdTAwMDdXgtlQXHUwMDFkVpj8SFxcUYihJFXlJJahOrG0QkyonzdWr+SokE4jVFx1MDAxNbK/XHUwMDFhN1634/gkg/meTFx1MDAwZiDlu2OfWb5h/cY3g91lR8vFJ3dJxSjnpm/y9/xx9+rub5B4Q/4ifQ== Docked widgetLayout widgets

    To dock a widget to an edge, add a dock: <EDGE>; declaration to it, where <EDGE> is one of top, right, bottom, or left. For example, a sidebar similar to that shown in the diagram above can be achieved using dock: left;. The code below shows a simple sidebar implementation.

    Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

    DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout1_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above).

    Docking multiple widgets to the same edge will result in overlap. The first widget yielded from compose will appear below widgets yielded after it. Let's dock a second sidebar, #another-sidebar, to the left of the screen. This new sidebar is double the width of the one previous one, and has a deeppink background.

    Outputdock_layout2_sidebar.pydock_layout2_sidebar.tcss

    DockLayoutExample Sidebar1Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. \u2587\u2587 Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout2_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar2\", id=\"another-sidebar\")\n        yield Static(\"Sidebar1\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\napp = DockLayoutExample()\nif __name__ == \"__main__\":\n    app.run()\n
    #another-sidebar {\n    dock: left;\n    width: 30;\n    height: 100%;\n    background: deeppink;\n}\n\n#sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    Notice that the original sidebar (#sidebar) appears on top of the newly docked widget. This is because #sidebar was yielded after #another-sidebar inside the compose method.

    Of course, we can also dock widgets to multiple edges within the same container. The built-in Header widget contains some internal CSS which docks it to the top. We can yield it inside compose, and without any additional CSS, we get a header fixed to the top of the screen.

    Outputdock_layout3_sidebar_header.pydock_layout3_sidebar_header.tcss

    DockLayoutExample Sidebar1DockLayoutExample Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout3_sidebar_header.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"header\")\n        yield Static(\"Sidebar1\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    If we wished for the sidebar to appear below the header, it'd simply be a case of yielding the sidebar before we yield the header.

    "},{"location":"guide/layout/#layers","title":"Layers","text":"

    Textual has a concept of layers which gives you finely grained control over the order widgets are placed.

    When drawing widgets, Textual will first draw on lower layers, working its way up to higher layers. As such, widgets on higher layers will be drawn on top of those on lower layers.

    Layer names are defined with a layers style on a container (parent) widget. Descendants of this widget can then be assigned to one of these layers using a layer style.

    The layers style takes a space-separated list of layer names. The leftmost name is the lowest layer, and the rightmost is the highest layer. Therefore, if you assign a descendant to the rightmost layer name, it'll be drawn on the top layer and will be visible above all other descendants.

    An example layers declaration looks like: layers: one two three;. To add a widget to the topmost layer in this case, you'd add a declaration of layer: three; to it.

    In the example below, #box1 is yielded before #box2. Given our earlier discussion on yield order, you'd expect #box2 to appear on top. However, in this case, both #box1 and #box2 are assigned to layers which define the reverse order, so #box1 is on top of #box2

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"guide/layout/#offsets","title":"Offsets","text":"

    Widgets have a relative offset which is added to the widget's location, after its location has been determined via its parent's layout. This means that if a widget hasn't had its offset modified using CSS or Python code, it will have an offset of (0, 0).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZWVPbSFx1MDAxMIDf+Vx1MDAxNZTzXHUwMDFhK3NcdTAwMWap2triXlxiS8JcdTAwMTVcYlspSkhjWWtZUqQx2Enx37clXHUwMDFjS744XGZhXHR6sK3p0Uxr5utr/GNpeblhXHUwMDA3qWm8X26YvudGoZ+5V423RfulyfIwiUFEyvs86WVe2bNtbZq/f/eu62ZcdTAwMWRj08j1jHNcdTAwMTnmPTfKbc9cdTAwMGZcdTAwMTPHS7rvQmu6+Z/F557bNX+kSde3mVNN0jR+aJPsZi5cdTAwMTOZroltXHUwMDBlo/9cdTAwMDP3y8s/ys+adpnxrFx1MDAxYlx1MDAwN5EpXHUwMDFmKEWVgpywyda9JC6VJVxcaM1cdTAwMDRBo1x1MDAwZWG+XHUwMDBl01njg7RcdTAwMDUqm0pSNDU2/L/pRvvbv70g+1x1MDAxNlxmVulgTZ82q1lbYVx1MDAxNFx1MDAxZNpBVGrlZUmeN9uu9dpVj9xmScechL5tQ1x1MDAxZjzRPno2T2AhqqeypFx1MDAxN7Rjk+djzySp64V2ULSh6lx1MDAxNW5cdTAwMTbi/XLV0i96UO4ohVx1MDAxNNdU8pGkeJYw7miGXHUwMDE5XHUwMDEyhE+os5ZEsFx0oM5cdTAwMWJUXpVCXHUwMDE3rtdcdECr2Fx1MDAxZvWxmVx1MDAxYuepm8FWVf2uhi/KtHCoVHpskrZcdIO2XHUwMDA1KSXKUayuWG7KLSCIYKWkpnokKWZNt/2Shq+Ty9d2s3S4TI28uKlpXFwou1FDqXq4l/ruzZZjIWA5XGJcdTAwMTDBdbV+UVx1MDAxOHdAXHUwMDE496Koaku8TkVJ2Xr9dlx1MDAwMTqxpvPoxJxgyTGW5N54XHUwMDFl2Y39081wV7ubXHUwMDFiJjv1z9OrvXl4TiA2XHUwMDBlJnlWMCVDhLJZYFx1MDAxMswmwHhyMJkzh0pcIlx1MDAxY0ww0jO4xEpQyonC9Dfm0kRRmOazqVx1MDAxNGoulVJcdTAwMTJcdTAwMDFb8lx1MDAwMCqT1Ytm/7CXx53Vg72TY4hcdTAwMDWfcGdcdTAwMTEqn9FdMvBXSlx0idUklZw5UmhO8ePc5ZuWy1x0J9NEYjJN/YhJjFx1MDAxZDYx9VxykUIrXHUwMDBlNqTx61x1MDAwNJKQuW5SIcxcdTAwMDVcdTAwMTX8/jz2XHUwMDBljoONwzPvUH6x4V/euWKnweZcdTAwMGLnUVwiR4PLmY7enDqTcXUxXHUwMDFhL1x1MDAxMOJPRaNUQkjxat0jkWKue0RCYK5FrcedOaV111vrQaY656q/x21rv1x1MDAxYixcdTAwMTS0n1x1MDAxMUdI55hcdTAwMTS8Tt1PXHUwMDFhXHUwMDFmjVwiJlx1MDAxN+B5n1xuRXDgiEtBlfyNWbw9hYQ0cS6OTCHN1Vx1MDAwM2hcXNvaOuqibbZ1XHUwMDE2fVx1MDAxOFxmdnbyj0r259DYdr12LzP/P49Eclx1MDAwN3EhxouYkkilXHUwMDFjOonq4uFazOJSI4fIMn+9XHUwMDE5iE7jKYnDsNRCSabLa1xuU1akXHUwMDE0XHUwMDE0iV9Q6VxmXHUwMDA1XHUwMDE1V7Xt3jnZXHUwMDBm6cpcdTAwMGU/kGebO1v+Jv34feXbaKwxXGLdLEuuXHUwMDFhI8n18NdcdTAwMGIxXHUwMDAyRvlcXCNcdTAwMTBcdTAwMTLyOMJqSe1dVtBstdbTXcp2dz+fb1xyzk74v51IvXQroFx1MDAxYUNcdTAwMTlccu+JucRIsYlUgVx1MDAxMeJcYqxcdTAwMWZf6Vx1MDAwZj30bFugo8OEhW2BgsdiYFxmr8pcdTAwMTRcbv+An8dcdTAwMTSEnpueXHUwMDEwpLDWhUO8tylE6+GXo1x1MDAwM2mClt7vtN0zkXhb+Us3hVwiICguXHUwMDE5marfXHUwMDE4l1x1MDAwZSNS0sdcdTAwMWUr3Fx1MDAxNlx1MDAxMVx1MDAxNHNcdTAwMTRReGhcdTAwMDNcYolcdTAwMDWsXHUwMDAwXHUwMDA24Fx1MDAwNKL373z0dWMnM931/FxmmjHQXHUwMDAxq/s769tccvdBhD7fuVx1MDAxN9RcdTAwMTDFiShDgKnCUvFxTGmR0eC7jlx1MDAxOZRs8YvFXHUwMDBmv6BYccCPa4Uxhe3neFx1MDAxYdPicJgzpYnSklx1MDAwYkbVJKZQknIsXHUwMDE5W6DUKzVdXHUwMDEw08J8yVx1MDAwMzCt6eFmdjWM/TBcdTAwMGVAWMWBn/8zbN8jXHIuqEq8Xrn7XHUwMDBllpD4gVx1MDAxOeNcItxcdTAwMDG8tU6Bm5ZEO1BcdTAwMTiVNfpQdD1Sx8T+3crcno3UlGlcIlx1MDAwN4FLwVx1MDAwNPJdpTVcdTAwMDRRxKbUUY7kRCvGOIJccuVCTilcdTAwMTW5uV1Lut3QwqJ/SsLYTi5uuYorhXW3jetPSuGl6rJJN5BcdTAwMTYjjofj6tdyZSflzej317cze89luLim6K1GW6p/z/Nf1vTtLPfF0S3uiyNYf0iB7u2/Lq8+dE6PV7+vN9m2/Cy7l3ZggpdcdTAwMWVhsdLgv1x1MDAwNNNMICaAqGpFSv8llFx1MDAwM9ZQxC+tMUTbx4TaW51YLauvju6nj1x1MDAwMSTkpFJcdTAwMTDyzMdcdTAwMDBcdTAwMTTXI9lcdTAwMDP8VCuJ7WH4/SZpXHUwMDFia910u2E0XHUwMDE427iSU9D0Y6uVXHUwMDFiW1/L3MCcJZdqrPdKXHUwMDE0XHUwMDA2cZnemdY44jb03GgktkntzT2Y3YXhsu0pk0+yMFxiYzc6XHUwMDFh1+RcdTAwMTFZLNdkro1xjFx1MDAwNUVS3d/G9pvHXHUwMDFl2ewrlVE//JCGe8dcdTAwMWLu2Vx1MDAxM9uYn1x1MDAxNP7yaZNcdTAwMDTmcKRnnLRRSVx1MDAxZHDv4tf+bfsk5Vx1MDAxYyZcZnKEwlx1MDAxYV5TPVfkyVx1MDAwZq/nloaDNtw0PbQw5Cjqw5qE/tDgq2FcdTAwMWGXoblanVV8lFehcmldXHUwMDA1wKZYkVx1MDAxZtdL1/9cdTAwMDE0elVbIn0= Offset

    The offset of a widget can be set using the offset CSS property. offset takes two values.

    • The first value defines the x (horizontal) offset. Positive values will shift the widget to the right. Negative values will shift the widget to the left.
    • The second value defines the y (vertical) offset. Positive values will shift the widget down. Negative values will shift the widget up.
    "},{"location":"guide/layout/#putting-it-all-together","title":"Putting it all together","text":"

    The sections above show how the various layouts in Textual can be used to position widgets on screen. In a real application, you'll make use of several layouts.

    The example below shows how an advanced layout can be built by combining the various techniques described on this page.

    Outputcombining_layouts.pycombining_layouts.tcss

    CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2585\u2585\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Header, Static\n\n\nclass CombiningLayoutsExample(App):\n    CSS_PATH = \"combining_layouts.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Container(id=\"app-grid\"):\n            with VerticalScroll(id=\"left-pane\"):\n                for number in range(15):\n                    yield Static(f\"Vertical layout, child {number}\")\n            with Horizontal(id=\"top-right\"):\n                yield Static(\"Horizontally\")\n                yield Static(\"Positioned\")\n                yield Static(\"Children\")\n                yield Static(\"Here\")\n            with Container(id=\"bottom-right\"):\n                yield Static(\"This\")\n                yield Static(\"panel\")\n                yield Static(\"is\")\n                yield Static(\"using\")\n                yield Static(\"grid layout!\", id=\"bottom-right-final\")\n\n\nif __name__ == \"__main__\":\n    app = CombiningLayoutsExample()\n    app.run()\n
    #app-grid {\n    layout: grid;\n    grid-size: 2;  /* two columns */\n    grid-columns: 1fr;\n    grid-rows: 1fr;\n}\n\n#left-pane > Static {\n    background: $boost;\n    color: auto;\n    margin-bottom: 1;\n    padding: 1;\n}\n\n#left-pane {\n    width: 100%;\n    height: 100%;\n    row-span: 2;\n    background: $panel;\n    border: dodgerblue;\n}\n\n#top-right {\n    height: 100%;\n    background: $panel;\n    border: mediumvioletred;\n}\n\n#top-right > Static {\n    width: auto;\n    height: 100%;\n    margin-right: 1;\n    background: $boost;\n}\n\n#bottom-right {\n    height: 100%;\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 1fr;\n    grid-rows: 1fr;\n    grid-gutter: 1;\n    background: $panel;\n    border: greenyellow;\n}\n\n#bottom-right-final {\n    column-span: 2;\n}\n\n#bottom-right > Static {\n    height: 100%;\n    background: $boost;\n}\n

    Textual layouts make it easy to design and build real-life applications with relatively little code.

    "},{"location":"guide/queries/","title":"DOM Queries","text":"

    In the previous chapter we introduced the DOM which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS selectors.

    Selectors are a very useful idea and can do more than apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!

    "},{"location":"guide/queries/#query-one","title":"Query one","text":"

    The query_one method gets a single widget in an app or other widget. If you call it with a selector it will return the first matching widget.

    Let's say we have a widget with an ID of send and we want to get a reference to it in our app. We could do this with the following:

    send_button = self.query_one(\"#send\")\n

    If there is no widget with an ID of send, Textual will raise a NoMatches exception. Otherwise it will return the matched widget.

    You can also add a second parameter for the expected type.

    send_button = self.query_one(\"#send\", Button)\n

    If the matched widget is not a button (i.e. if isinstance(widget, Button) equals False), Textual will raise a WrongType exception.

    Tip

    The second parameter allows type-checkers like MyPy to know the exact return type. Without it, MyPy would only know the result of query_one is a Widget (the base class).

    "},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

    Apps and widgets have a query method which finds (or queries) widgets. This method returns a DOMQuery object which is a list-like container of widgets.

    If you call query with no arguments, you will get back a DOMQuery containing all widgets. This method is recursive, meaning it will also return child widgets (as many levels as required).

    Here's how you might iterate over all the widgets in your app:

    for widget in self.query():\n    print(widget)\n

    Called on the app, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.

    Note

    All the query and related methods work on both App and Widget sub-classes.

    "},{"location":"guide/queries/#query-selectors","title":"Query selectors","text":"

    You can call query with a CSS selector. Let's look a few examples:

    If we want to find all the button widgets, we could do something like the following:

    for button in self.query(\"Button\"):\n    print(button)\n

    Any selector that works in CSS will work with the query method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:

    for button in self.query(\"Dialog Button.disabled\"):\n    print(button)\n

    Info

    The selector Dialog Button.disabled says find all the Button with a CSS class of disabled that are a child of a Dialog widget.

    "},{"location":"guide/queries/#results","title":"Results","text":"

    Query objects have a results method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.

    The following example queries for widgets with the disabled CSS class and iterates over just the Button objects.

    for button in self.query(\".disabled\").results(Button):\n    print(button)\n

    Tip

    This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that button is a Widget (the base class).

    "},{"location":"guide/queries/#query-objects","title":"Query objects","text":"

    We've seen that the query method returns a DOMQuery object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as query[0], len(query) ,reverse(query) etc). They also have a number of other methods to simplify retrieving and modifying widgets.

    "},{"location":"guide/queries/#first-and-last","title":"First and last","text":"

    The first and last methods return the first or last matching widget from the selector, respectively.

    Here's how we might find the last button in an app:

    last_button = self.query(\"Button\").last()\n

    If there are no buttons, Textual will raise a NoMatches exception. Otherwise it will return a button widget.

    Both first() and last() accept an expect_type argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class .disabled, and we want to check it really is a button. We could do this:

    disabled_button = self.query(\".disabled\").last(Button)\n

    The query selects all widgets with a disabled CSS class. The last method gets the last disabled widget and checks it is a Button and not any other kind of widget.

    If the last widget is not a button, Textual will raise a WrongType exception.

    Tip

    Specifying the expected type allows type-checkers like MyPy to know the exact return type.

    "},{"location":"guide/queries/#filter","title":"Filter","text":"

    Query objects have a filter method which further refines a query. This method will return a new query object with widgets that match both the original query and the new selector.

    Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:

    # Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Buttons with 'disabled' CSS class\ndisabled_buttons = buttons_query.filter(\".disabled\")\n

    Iterating over disabled_buttons will give us all the disabled buttons.

    "},{"location":"guide/queries/#exclude","title":"Exclude","text":"

    Query objects have an exclude method which is the logical opposite of filter. The exclude method removes any widgets from the query object which match a selector.

    Here's how we could get all the buttons which don't have the disabled class set.

    # Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Remove all the Buttons with the 'disabled' CSS class\nenabled_buttons = buttons_query.exclude(\".disabled\")\n
    "},{"location":"guide/queries/#loop-free-operations","title":"Loop-free operations","text":"

    Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.

    For instance, let's say we want to disable all buttons in an app. We could do this by calling add_class() on a query object.

    self.query(\"Button\").add_class(\"disabled\")\n

    This single line is equivalent to the following:

    for widget in self.query(\"Button\"):\n    widget.add_class(\"disabled\")\n

    Here are the other loop-free methods on query objects:

    • set_class Sets a CSS class (or classes) on matched widgets.
    • add_class Adds a CSS class (or classes) to matched widgets.
    • remove_class Removes a CSS class (or classes) from matched widgets.
    • toggle_class Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
    • remove Removes matched widgets from the DOM.
    • refresh Refreshes matched widgets.
    "},{"location":"guide/reactivity/","title":"Reactivity","text":"

    Textual's reactive attributes are attributes with superpowers. In this chapter we will look at how reactive attributes can simplify your apps.

    Quote

    With great power comes great responsibility.

    \u2014 Uncle Ben

    "},{"location":"guide/reactivity/#reactive-attributes","title":"Reactive attributes","text":"

    Textual provides an alternative way of adding attributes to your widget or App, which doesn't require adding them to your class constructor (__init__). To create these attributes import reactive from textual.reactive, and assign them in the class scope.

    The following code illustrates how to create reactive attributes:

    from textual.reactive import reactive\nfrom textual.widget import Widget\n\nclass Reactive(Widget):\n\n    name = reactive(\"Paul\")  # (1)!\n    count = reactive(0) # (2)!\n    is_cool = reactive(True)  # (3)!\n
    1. Create a string attribute with a default of \"Paul\"
    2. Creates an integer attribute with a default of 0.
    3. Creates a boolean attribute with a default of True.

    The reactive constructor accepts a default value as the first positional argument.

    Information

    Textual uses Python's descriptor protocol to create reactive attributes, which is the same protocol used by the builtin property decorator.

    You can get and set these attributes in the same way as if you had assigned them in an __init__ method. For instance self.name = \"Jessica\", self.count += 1, or print(self.is_cool).

    "},{"location":"guide/reactivity/#dynamic-defaults","title":"Dynamic defaults","text":"

    You can also set the default to a function (or other callable). Textual will call this function to get the default value. The following code illustrates a reactive value which will be automatically assigned the current time when the widget is created:

    from time import time\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\n\nclass Timer(Widget):\n\n    start_time = reactive(time)  # (1)!\n
    1. The time function returns the current time in seconds.
    "},{"location":"guide/reactivity/#typing-reactive-attributes","title":"Typing reactive attributes","text":"

    There is no need to specify a type hint if a reactive attribute has a default value, as type checkers will assume the attribute is the same type as the default.

    You may want to add explicit type hints if the attribute type is a superset of the default type. For instance if you want to make an attribute optional. Here's how you would create a reactive string attribute which may be None:

        name: reactive[str | None] = reactive(\"Paul\")\n
    "},{"location":"guide/reactivity/#smart-refresh","title":"Smart refresh","text":"

    The first superpower we will look at is \"smart refresh\". When you modify a reactive attribute, Textual will make note of the fact that it has changed and refresh automatically.

    Information

    If you modify multiple reactive attributes, Textual will only do a single refresh to minimize updates.

    Let's look at an example which illustrates this. In the following app, the value of an input is used to update a \"Hello, World!\" type greeting.

    refresh01.pyrefresh01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\")\n\n    def render(self) -> str:\n        return f\"Hello, {self.who}!\"\n\n\nclass WatchApp(App):\n    CSS_PATH = \"refresh01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter your name\")\n        yield Name()\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        self.query_one(Name).who = event.value\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\nName {\n    height: 100%;\n    content-align: center middle;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aTextual\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Hello,\u00a0Textual!

    The Name widget has a reactive who attribute. When the app modifies that attribute, a refresh happens automatically.

    Information

    Textual will check if a value has really changed, so assigning the same value wont prompt an unnecessary refresh.

    "},{"location":"guide/reactivity/#disabling-refresh","title":"Disabling refresh","text":"

    If you don't want an attribute to prompt a refresh or layout but you still want other reactive superpowers, you can use var to create an attribute. You can import var from textual.reactive.

    The following code illustrates how you create non-refreshing reactive attributes.

    class MyWidget(Widget):\n    count = var(0)  # (1)!\n
    1. Changing self.count wont cause a refresh or layout.
    "},{"location":"guide/reactivity/#layout","title":"Layout","text":"

    The smart refresh feature will update the content area of a widget, but will not change its size. If modifying an attribute should change the size of the widget, you can set layout=True on the reactive attribute. This ensures that your CSS layout will update accordingly.

    The following example modifies \"refresh01.py\" so that the greeting has an automatic width.

    refresh02.pyrefresh02.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\", layout=True)  # (1)!\n\n    def render(self) -> str:\n        return f\"Hello, {self.who}!\"\n\n\nclass WatchApp(App):\n    CSS_PATH = \"refresh02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter your name\")\n        yield Name()\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        self.query_one(Name).who = event.value\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    1. This attribute will update the layout when changed.
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\nName {\n    width: auto;\n    height: auto;\n    border: heavy $secondary;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aname\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Hello,\u00a0name!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    If you type in to the input now, the greeting will expand to fit the content. If you were to set layout=False on the reactive attribute, you should see that the box remains the same size when you type.

    "},{"location":"guide/reactivity/#validation","title":"Validation","text":"

    The next superpower we will look at is validation, which can check and potentially modify a value you assign to a reactive attribute.

    If you add a method that begins with validate_ followed by the name of your attribute, it will be called when you assign a value to that attribute. This method should accept the incoming value as a positional argument, and return the value to set (which may be the same or a different value).

    A common use for this is to restrict numbers to a given range. The following example keeps a count. There is a button to increase the count, and a button to decrease it. The validation ensures that the count will never go above 10 or below zero.

    validate01.pyvalidate01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, RichLog\n\n\nclass ValidateApp(App):\n    CSS_PATH = \"validate01.tcss\"\n\n    count = reactive(0)\n\n    def validate_count(self, count: int) -> int:\n        \"\"\"Validate value.\"\"\"\n        if count < 0:\n            count = 0\n        elif count > 10:\n            count = 10\n        return count\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Button(\"+1\", id=\"plus\", variant=\"success\"),\n            Button(\"-1\", id=\"minus\", variant=\"error\"),\n            id=\"buttons\",\n        )\n        yield RichLog(highlight=True)\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"plus\":\n            self.count += 1\n        else:\n            self.count -= 1\n        self.query_one(RichLog).write(f\"count = {self.count}\")\n\n\nif __name__ == \"__main__\":\n    app = ValidateApp()\n    app.run()\n
    #buttons {\n    dock: top;\n    height: auto;\n}\n

    ValidateApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 +1-1 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    If you click the buttons in the above example it will show the current count. When self.count is modified in the button handler, Textual runs validate_count which performs the validation to limit the value of count.

    "},{"location":"guide/reactivity/#watch-methods","title":"Watch methods","text":"

    Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch method names begin with watch_ followed by the name of the attribute, and should accept one or two arguments. If the method accepts a single argument, it will be called with the new assigned value. If the method accepts two positional arguments, it will be called with both the old value and the new value.

    The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example \"darkorchid\" or \"#52de44\".

    watch01.pywatch01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.color import Color, ColorParseError\nfrom textual.containers import Grid\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\n\n\nclass WatchApp(App):\n    CSS_PATH = \"watch01.tcss\"\n\n    color = reactive(Color.parse(\"transparent\"))  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a color\")\n        yield Grid(Static(id=\"old\"), Static(id=\"new\"), id=\"colors\")\n\n    def watch_color(self, old_color: Color, new_color: Color) -> None:  # (2)!\n        self.query_one(\"#old\").styles.background = old_color\n        self.query_one(\"#new\").styles.background = new_color\n\n    def on_input_submitted(self, event: Input.Submitted) -> None:\n        try:\n            input_color = Color.parse(event.value)\n        except ColorParseError:\n            pass\n        else:\n            self.query_one(Input).value = \"\"\n            self.color = input_color  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    1. Creates a reactive color attribute.
    2. Called when self.color is changed.
    3. New color is assigned here.
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\n#colors {\n    grid-size: 2 1;\n    grid-gutter: 2 4;\n    grid-columns: 1fr;\n    margin: 0 1;\n}\n\n#old {\n    height: 100%;\n    border: wide $secondary;\n}\n\n#new {\n    height: 100%;\n    border: wide $secondary;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adarkorchid\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    The color is parsed in on_input_submitted and assigned to self.color. Because color is reactive, Textual also calls watch_color with the old and new values.

    "},{"location":"guide/reactivity/#when-are-watch-methods-called","title":"When are watch methods called?","text":"

    Textual only calls watch methods if the value of a reactive attribute changes. If the newly assigned value is the same as the previous value, the watch method is not called. You can override this behaviour by passing always_update=True to reactive.

    "},{"location":"guide/reactivity/#dynamically-watching-reactive-attributes","title":"Dynamically watching reactive attributes","text":"

    You can programmatically add watchers to reactive attributes with the method watch. This is useful when you want to react to changes to reactive attributes for which you can't edit the watch methods.

    The example below shows a widget Counter that defines a reactive attribute counter. The app that uses Counter uses the method watch to keep its progress bar synced with the reactive attribute:

    dynamic_watch.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Button, Label, ProgressBar\n\n\nclass Counter(Widget):\n    DEFAULT_CSS = \"Counter { height: auto; }\"\n    counter = reactive(0)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Label()\n        yield Button(\"+10\")\n\n    def on_button_pressed(self) -> None:\n        self.counter += 10\n\n    def watch_counter(self, counter_value: int):\n        self.query_one(Label).update(str(counter_value))\n\n\nclass WatchApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield ProgressBar(total=100, show_eta=False)\n\n    def on_mount(self):\n        def update_progress(counter_value: int):  # (2)!\n            self.query_one(ProgressBar).update(progress=counter_value)\n\n        self.watch(self.query_one(Counter), \"counter\", update_progress)  # (3)!\n\n\nif __name__ == \"__main__\":\n    WatchApp().run()\n
    1. counter is a reactive attribute defined inside Counter.
    2. update_progress is a custom callback that will update the progress bar when counter changes.
    3. We use the method watch to set update_progress as an additional watcher for the reactive attribute counter.

    WatchApp 30 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 +10 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250130%

    "},{"location":"guide/reactivity/#compute-methods","title":"Compute methods","text":"

    Compute methods are the final superpower offered by the reactive descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with compute_ followed by the name of the reactive value.

    You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.

    The following example uses a computed attribute. It displays three inputs for each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.

    computed01.pycomputed01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\n\n\nclass ComputedApp(App):\n    CSS_PATH = \"computed01.tcss\"\n\n    red = reactive(0)\n    green = reactive(0)\n    blue = reactive(0)\n    color = reactive(Color.parse(\"transparent\"))\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Input(\"0\", placeholder=\"Enter red 0-255\", id=\"red\"),\n            Input(\"0\", placeholder=\"Enter green 0-255\", id=\"green\"),\n            Input(\"0\", placeholder=\"Enter blue 0-255\", id=\"blue\"),\n            id=\"color-inputs\",\n        )\n        yield Static(id=\"color\")\n\n    def compute_color(self) -> Color:  # (1)!\n        return Color(self.red, self.green, self.blue).clamped\n\n    def watch_color(self, color: Color) -> None:  # (2)\n        self.query_one(\"#color\").styles.background = color\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        try:\n            component = int(event.value)\n        except ValueError:\n            self.bell()\n        else:\n            if event.input.id == \"red\":\n                self.red = component\n            elif event.input.id == \"green\":\n                self.green = component\n            else:\n                self.blue = component\n\n\nif __name__ == \"__main__\":\n    app = ComputedApp()\n    app.run()\n
    1. Combines color components in to a Color object.
    2. The watch method is called when the result of compute_color changes.
    #color-inputs {\n    dock: top;\n    height: auto;\n}\n\nInput {\n    width: 1fr;\n}\n\n#color {\n    height: 100%;\n    border: tall $secondary;\n}\n

    ComputedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a0\u258e\u258a0\u258e\u258a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the compute_color method which combines the color components into a Color object. It will be recalculated when any of the red , green, or blue attributes are modified.

    When the result of compute_color changes, Textual will also call watch_color since color still has the watch method superpower.

    Note

    Textual will first attempt to call the compute method for a reactive attribute, followed by the validate method, and finally the watch method.

    Note

    It is best to avoid doing anything slow or CPU-intensive in a compute method. Textual calls compute methods on an object when any reactive attribute changes.

    "},{"location":"guide/screens/","title":"Screens","text":"

    This chapter covers Textual's screen API. We will discuss how to create screens and switch between them.

    "},{"location":"guide/screens/#what-is-a-screen","title":"What is a screen?","text":"

    Screens are containers for widgets that occupy the dimensions of your terminal. There can be many screens in a given app, but only one screen is active at a time.

    Textual requires that there be at least one screen object and will create one implicitly in the App class. If you don't change the screen, any widgets you mount or compose will be added to this default screen.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()"},{"location":"guide/screens/#creating-a-screen","title":"Creating a screen","text":"

    You can create a screen by extending the Screen class which you can import from textual.screen. The screen may be styled in the same way as other widgets, with the exception that you can't modify the screen's dimensions (as these will always be the size of your terminal).

    Let's look at a simple example of writing a screen class to simulate Window's blue screen of death.

    screen01.pyscreen01.tcssOutput screen01.py
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\n\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\n\nPress Enter to return to Windows, or\n\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\n\nError: 0E : 016F : BFF9B3D4\n\"\"\"\n\n\nclass BSOD(Screen):\n    BINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\n\n    def compose(self) -> ComposeResult:\n        yield Static(\" Windows \", id=\"title\")\n        yield Static(ERROR_TEXT)\n        yield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\n\n\nclass BSODApp(App):\n    CSS_PATH = \"screen01.tcss\"\n    SCREENS = {\"bsod\": BSOD()}\n    BINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\n\n\nif __name__ == \"__main__\":\n    app = BSODApp()\n    app.run()\n
    screen01.tcss
    BSOD {\n    align: center middle;\n    background: blue;\n    color: white;\n}\n\nBSOD>Static {\n    width: 70;\n}\n\n#title {\n    content-align-horizontal: center;\n    text-style: reverse;\n}\n\n#any-key {\n    content-align-horizontal: center;\n}\n

    BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

    If you run this you will see an empty screen. Hit the B key to show a blue screen of death. Hit Esc to return to the default screen.

    The BSOD class above defines a screen with a key binding and compose method. These should be familiar as they work in the same way as apps.

    The app class has a new SCREENS class variable. Textual uses this class variable to associate a name with screen object (the name is used to reference screens in the screen API). Also in the app is a key binding associated with the action \"push_screen('bsod')\". The screen class has a similar action \"pop_screen\" bound to the Esc key. We will cover these actions below.

    "},{"location":"guide/screens/#named-screens","title":"Named screens","text":"

    You can associate a screen with a name by defining a SCREENS class variable in your app, which should be a dict that maps names on to Screen objects. The name of the screen may be used interchangeably with screen objects in much of the screen API.

    You can also install new named screens dynamically with the install_screen method. The following example installs the BSOD screen in a mount handler rather than from the SCREENS variable.

    screen02.pyscreen02.tcssOutput screen02.py
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\n\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\n\nPress Enter to return to Windows, or\n\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\n\nError: 0E : 016F : BFF9B3D4\n\"\"\"\n\n\nclass BSOD(Screen):\n    BINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\n\n    def compose(self) -> ComposeResult:\n        yield Static(\" Windows \", id=\"title\")\n        yield Static(ERROR_TEXT)\n        yield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\n\n\nclass BSODApp(App):\n    CSS_PATH = \"screen02.tcss\"\n    BINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\n\n    def on_mount(self) -> None:\n        self.install_screen(BSOD(), name=\"bsod\")\n\n\nif __name__ == \"__main__\":\n    app = BSODApp()\n    app.run()\n
    screen02.tcss
    BSOD {\n    align: center middle;\n    background: blue;\n    color: white;\n}\n\nBSOD>Static {\n    width: 70;\n}\n\n#title {\n    content-align-horizontal: center;\n    text-style: reverse;\n}\n\n#any-key {\n    content-align-horizontal: center;\n}\n

    BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

    Although both do the same thing, we recommend SCREENS for screens that exist for the lifetime of your app.

    "},{"location":"guide/screens/#uninstalling-screens","title":"Uninstalling screens","text":"

    Screens defined in SCREENS or added with install_screen are installed screens. Textual will keep these screens in memory for the lifetime of your app.

    If you have installed a screen, but you later want it to be removed and cleaned up, you can call uninstall_screen.

    "},{"location":"guide/screens/#screen-stack","title":"Screen stack","text":"

    Textual apps keep a stack of screens. You can think of this screen stack as a stack of paper, where only the very top sheet is visible. If you remove the top sheet, the paper underneath becomes visible. Screens work in a similar way.

    Note

    You can also make parts of the top screen translucent, so that deeper screens show through. See Screen opacity.

    The active screen (top of the stack) will render the screen and receive input events. The following API methods on the App class can manipulate this stack, and let you decide which screen the user can interact with.

    "},{"location":"guide/screens/#push-screen","title":"Push screen","text":"

    The push_screen method puts a screen on top of the stack and makes that screen active. You can call this method with the name of an installed screen, or a screen object.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVcXG1z2khcdTAwMTL+nl/h8n3Zq1xu2nnr6Zmturpcbk5cdTAwMWN7XHUwMDEzx/Fb/HK35ZJBgNaAMFx1MDAxMrbxVv779chcdTAwMGVcdTAwMTIvXCJgMEtyVGyMRkitmaeffrpnJn+92tjYTPqdYPO3jc3gvuI3w2rXv9t87Y7fXHUwMDA23TiM2tQk0s9x1OtW0jNcdTAwMWJJ0ol/+/XXlt+9XHUwMDBlkk7Tr1x1MDAwNN5tXHUwMDE49/xmnPSqYeRVotavYVx1MDAxMrTif7vfn/xW8K9O1KomXS+7SSmohknUfbxX0FxmWkE7ienq/6HPXHUwMDFiXHUwMDFif6W/c9ZVQ79cdTAwMTW1q+npaUPOPLSjRz9F7dRUY5hgWqFcdTAwMWGcXHUwMDEwxm/pZklQpdZcdTAwMWFcdTAwMTlcdTAwMWNkLe7QJjQv9MPZWalafuj/WTqqmebJXHUwMDE3nt21XHUwMDE2NptHSb+Z2lx1MDAxNEf0KFlbnHSj6+A0rCZccmrlI8eLvtWNevVGO4jjoe9EXHUwMDFkv1x1MDAxMiZ9d4yxwVG/XU+vkVx1MDAxZLmnT4prjzFccmgtN1x1MDAwMCx7WPd9oYzHuFVWgFx1MDAxMlpxgSOGbUVNXHUwMDFhXHUwMDA2MuxcdTAwMWYsfWWmXfmV6zrZ165m53Dw/aua1tlZd09cdTAwMGasLHjKKIkwaGpcdTAwMDRhvZE4I4z2XGZyZvOtcZCOXHUwMDAyV1ZbhsCzXHUwMDE2d9PObjWFw1x1MDAxZqP92PC7naf+2ozdh5zBztZ3o1jK4yk30sFu9fz86Fg97Fx1MDAxZVx1MDAxZX5IToKqvdy6XHUwMDFmXFxrXGJ8frdcdTAwMWLdbVx1MDAwZVq+Pv2VmdbrVP1HTHGtlWVcdTAwMTJcZkeW9XQzbF9TY7vXbGbHosp1XHUwMDA2w/To19dzg19cdFVcdTAwMDR+Qo7Qmis5O/rvoHFwvV+PXHUwMDBmPnROXHK7XHUwMDEwd1dvP/TWXHUwMDFj/YJ5wjKu6FGNNiiH4c9Re1xujVx1MDAwNcGNsoh8MfjX/CvGYIng18BcdTAwMDRYo/lqwVx1MDAxZuvz8Kh8X98qb5v7crdzqe3t4VLAb1xmXHUwMDFhLkGwZYE/XHTuk0nIXHUwMDA3XHKFyDdGXHUwMDEx+Fx0XHUwMDBmMyNfto9iXHUwMDFktMzno3q3vHuO8mRfrDnvXHUwMDFiXHSe0VJoXHUwMDAzipxcdTAwMWPsXHUwMDEw8ClcdTAwMTB4TCgujaBcdTAwMWZccrBcdTAwMTDu0Vx1MDAwMquJcdxzZsZcdTAwMDGPfFxm5lx1MDAxNH6YlIrJn4fjhbZKSJhcdTAwMDPmXHUwMDE5mqJ2clx1MDAxND5cdTAwMDQpOVxmXHUwMDFk3fZbYbM/XHUwMDA0iVx1MDAxNP9k4FGlXHUwMDFiXHUwMDA07Vxy/t/2L42wWlxy2v/MXHUwMDBmWVx1MDAxY9D93Vx1MDAwNfXwN980w7rzls1mUFx1MDAxYnajJCQpNmhOolxcXHUwMDFmV8hcdTAwMTKfLtfdrY4+UdRccuth229cdTAwMWVcdTAwMTdb9Sxv1kZcdTAwMTR7M1xigUTgXHUwMDE5/L/nzVx1MDAxNyfbd35n//D+Zlx1MDAwN45bV+clXHUwMDE5Xa+5Nyu0XHUwMDFlSoZWUawg+squ4r5P4cVcdTAwMTDgkFx0ozSSWHpcdTAwMTFvXHUwMDE2MiPMgTfnjj15s2aS5FxymiyU/uAxy1x0Nupblj3pipxZbPxCiVN41VxmJjuzgKFvrsiZ81ZNdebHbp7gzVxc5eLCqDtTTFKkj0Xm8N9z5+kjP4c7i1Fsvpg7g9RcdTAwMWVcdTAwMThtKTZLhZSaXHKrUqU8y8iVXHUwMDA1Q2lAczli2HL8XHUwMDE50JOcXiR/XHUwMDE13Vx1MDAwYsxcdTAwMDT3RuY5cVxmkqhF0082bt9it6A2inXyXHUwMDE5+Vlq51x1MDAxNHf/nkPOk0Hl7PC7STlsV8N2nVx1MDAxYTMm+VZm2J0hRqQuXFzpOSuZp4FzSYNI6Vx1MDAwNVx09ixRdV3hd5zN0mOGRFx1MDAwZadMg34o6Xo64+vAqqBd/b5N0/OvIZtcdTAwMTgpXFxOXHRccpL2s1xcXHQ7Zlx1MDAxNNmkZZrzcCYkmT5mU9OPk62o1VxuXHUwMDEz6vrPUdhORrs47cs3zs1cdTAwMWKBP8ZcdTAwMWb0TPm2UT7ouCtcdTAwMGXTevbXRuYw6YfB33+8nnh2MZbda1xmxdnlXuXf52Yyul0hkZGsZsRcdTAwMDSQucz3iGy6XHUwMDFlXUtcIjPUtYpcdTAwMGLrKkdEMyp72DTLkOhRzoeugEN5iFUjdi2HxzR6hmv6p4GowfKMTFx1MDAwNzRmXGJcdTAwMDCaXHUwMDEyaVdH4qBzicZcdTAwMTONSUtcdTAwMDJcdTAwMTJgxSSGXCLPqMsnselp61x1MDAxMGFI5ViCXHUwMDEzYl1HWZM76YnEhCdcdTAwMTEoWilwWog/l8Sml1BzNpXIKGlpXFy4pZtcdTAwMDGiVuNGecagdIqBMkgk++GHZrFSIZTT1jFcdTAwMTTPSWNTXG6F0rDRo1x1MDAwM1wi05K7bFx1MDAxNmZPsPzfXHUwMDBm3vfffLx4+yG86PcvL9tcdTAwMGbNXHUwMDAztt5cdFx1MDAxNiFcdTAwMWY8XHUwMDA1oDSjIE3cnYmhtE5cdTAwMGXCQ8s1UlJPRJZcdTAwMTNsa1ImJ7BokCBeoExeXFzLI6fjdq5cIsdz8Vx0qjBjUOT3zpjZq3kx6NPLclx1MDAxZuO9m/2y6uzU7vq4t/bwtJ6SXHUwMDAyXFzh0lx1MDAwMojhQMuN8Fx1MDAxOKN2XHUwMDEyQYyyXG47qlx1MDAwMP7eOjblXGJcdTAwMDBMw1x1MDAwYpSxp1TgrFwiPWJXXHUwMDAwTi1kXHUwMDExOEFKrTnD2UXgVrVcdTAwMWQlW5c7+zbeerhcdTAwMGXOeVJcdTAwMGab61x1MDAwZU4hPFLXRlJ4XHUwMDAyru1wdaqktYdcdTAwMTTTSGExJPDgYjLQiIrlwVx1MDAxMqkzzcCZYSueYTRBsNP7+PDQvLrQlzWZhLJ/wXJCaCz/XHUwMDE4tHx9Pe26+/H+obi77iad29Ln2t71l/v34tNcdTAwMTKuW8Wbm+B3/Vx0opCfRNt7N3uldydLuO5N+fN+VZ2etu7Ptt9WoFx1MDAwYu/fXHUwMDA0/WVV4Y1GsHJZXHUwMDFjUFSelpJcdTAwMTdcdTAwMTFcdTAwMDBSVkHaWsqZXHTg0n93/mf/rnZcdTAwMTi/3T/dub1Oro72dtY7XHUwMDBipDOMR1x1MDAxOZ5kLolcdTAwMTJsZJa1RKmFJ4hcYkmju0lYu2BcIkhZ01UwQTxBbrZ74PpqzOGVm/cmmbdSqYRcYozPXHUwMDE1jbJcdTAwMTHPSsiUT3NDbKtJoXKj7dA5g4JyhrVvXHUwMDA1Zb/T8Tq9uHFcdTAwMTmnNdxfXHUwMDFl3+TksnKupL+KsnKhbVNdsbAkI6coRZe1XHUwMDAym10oTqe8Zbhi1Y9cdTAwMWLBsoOx9ijUSlxutIYh48PB2IBcdTAwMDecXHUwMDE5amTkhsYsNvFb5IncXHUwMDEzzsW04kZxd5dcdI7JuSRSsKBpRCwl72JsJom7iVx1MDAwMiZI187vqc8vy5BuJLvnWaCQs2Omssx0ibeRL8tYo5hQSlCqRfwhclXNp1xuXGJ4Wlxu5UaUXHUwMDEz/VEnPp1QUJVcdTAwMTl+ilx1MDAxZqg0UoyotHVcZkvZ9V7l3+emXHUwMDEzZVxuQztHkETtYlx1MDAwZXE/XeusKaFcdTAwMDBcdTAwMDePUkpcdEaCXCKVn027pISiSFkjXG5cdTAwMDDNgVx1MDAxOGexuapcIkJcdTAwMTFcdTAwMWVQnm+UIHfgUptcdFx1MDAxYZ9z7qFby6WsK7vz3ErHgdJHXHUwMDFhM3jWcqpF+IS6jWfG/I18UlwiQrGCuVVBjjGktpg765FQKHhcYkB3krbaMGV/UkIpRJR7jWNpWXxC2UAhnyhcdTAwMDZMXG4+R6F1eq63pnxcIjR6qJTRmlx1MDAxMiPQw3RcIoT1pFx1MDAxNVppXHUwMDFhXHUwMDE4o5larJJVLFAsY5xuT9lcbpOcTZj6pifxSEXRXHUwMDE1LKUsbv52lE9QXHUwMDExMii1y1x1MDAxYVZCJ0ipy0vOXHUwMDFhzSFPSJxJKy2iXHUwMDA2olxuXHUwMDFh1DE6sVx1MDAxZadcdTAwMGUkkyl40Kjit6nXn41OXG5cdTAwMDGVNo5BaU46mbbEu3ihK7FcdTAwMWKSaJqjMv7pTInTqFxcOjg9tlx1MDAwZqdYPWbnX2prXnwkXHTmKTBGK0U9j7lcdTAwMTn5lE8096hcdTAwMDeMJFVohFx1MDAxNHKx0sNLbHBAXHUwMDEy+EOZ2ErKj0dcdTAwMTf7b5p7R8FJr1x1MDAwM+XD6qfT3avP0bLKbujkRcbcL1h6l4XhVEmwXHUwMDFhQc++kKxxU2p3XHUwMDBmto7fn15Hl5dxq9xTbz+sO/qBMlx1MDAxZlx1MDAwNsg5PaxQo/tcdTAwMWIs86RbdUSpXHUwMDEx0zQq6zUvxLlgXHUwMDE0RC0+I4IuMDGElph4XHUwMDFlQf5cXHSiLlxctmyF0qTF5+Dmz7+fd9iX3bPem/P6Wb9S7d10+s8qRq1cdTAwMTSdgjRcdTAwMDJ1ttRcdTAwMWElz3XH41x1MDAwNZRcdTAwMDdcXKBcdTAwMTKkXHUwMDExXHUwMDAw0C6WOy59Yki4WXVjV7wrYYF5oe9cdTAwMTKzJI3CzNKIuWg+RPDiJMcyRVx1MDAwMlxi7ezbzirJznZ8dVx1MDAxYpr7sniItpvxzVFcdTAwMTfWPckxaDw3XHUwMDExQsLEas1gWJWUXHUwMDA0+Vx1MDAwNaBUaaqthF5s31lcdTAwMTHy81x1MDAxYlCmrNjnXGYkokSxYqSrOHp7v7d3wpPjg6D8caeCeLK/XHUwMDE0pFPmzjVH8ayqy1wiS/blWi7Zl1x1MDAwYi/ZXHUwMDA3LC6DOpaXgs1cdTAwMTHKplx1MDAwZvx6TnFazTyBXHUwMDAyJMFcbpRcdTAwMWTZRq09bi0pXHUwMDE5Zlx1MDAwNXk7vkwgk5LuopVA6TJcdTAwMWGYtFx1MDAxZMcqz1xiSomsUW5cdTAwMTNcdTAwMGVcdTAwMWL3dVx1MDAwMaBcZua3XHUwMDFmr2Kp69xxJ2fHTEWL6UFiI1+0oFx1MDAwNF1cdTAwMGLLSSy7moTiuZNcdTAwMDZLXVx1MDAwNWjDnO3I3L7EpzN+tqJFqVx1MDAxMFHuNYal7HKv8u9zq1x1MDAwM1moio3klKHjXHUwMDFje/mOXHUwMDBm3vdKeHi6/eXjx/pd/fL0T41FS01cdTAwMWJ+pdHrXHUwMDA266CL0Vx1MDAxNYRAWMNcdTAwMTQoPZK1XHUwMDAxqWYrgCSElJbU68uIg9zE1jRtXHUwMDAwlMhz58Gr1Vx1MDAwNrUusWjnXGaO3zW2XHUwMDBmv1x1MDAxY4ja4bugM5s2eD3tui9a9rDWTZCtTHM8bqldXHUwMDA3nfFkyfO0XHUwMDA1isL/ocWme+D4XHUwMDFjM6zTcTNcdTAwMTdcdTAwMWasUFxcaItcdTAwMWVBR9Cjulx1MDAxZPt2ZFx1MDAxYo1cdTAwMTJcdTAwMWVoXHUwMDA13M2aUFx1MDAxZbtYXHUwMDExpzBZXHUwMDAwj9PFXHLnkrJxJifJXHUwMDBiro1cdTAwMDeIRksrXHUwMDA1XHUwMDEy6Mc20qCbu2GYXHUwMDFisZXMiTzb8WaUXHUwMDE308PMxvCuXHUwMDE1t0NDS+pAt0JcdTAwMDAmLNrgdJLmMt1qwMBcdTAwMDD8rFx1MDAwMqNcdTAwMThT7lVcdTAwMWGH05xcdTAwMTKjkFM0K+RcdTAwMTTQbjGylLNrjOkxY105XHUwMDA1085329qY0E5VjXCKJVx1MDAwNWKYNcYoXHUwMDE0L7Qm2yjPpYaoLaOsg7p9nFLIXHUwMDBlt1KNJCjRXHUwMDFiSW4xXoejhEpxQlHWef9/nFwiaFx1MDAxOK2rXHUwMDE4XHUwMDE56lx1MDAwYs3HOUW6rXCIlIQq1EaQfpzOKUVWTZ9cdTAwMDJcdTAwMWOxijFlJLOKI1GZlVx1MDAxM5jOc5UwTckxxVx1MDAwNUvZxI+9Qa9cdTAwMTDP7lVcdTAwMWGHclx1MDAxMZ29erqDW/x6lFx1MDAxMO5cdTAwMDZcdTAwMDNC0Fx1MDAwZatPSjB7zM3bMLgrT5qRSV9OeKVcdTAwMWTquChwXHUwMDBm+9fXV1//XHUwMDA3mNhRliJ9 Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden"},{"location":"guide/screens/#action","title":"Action","text":"

    You can also push screens with the \"app.push_screen\" action, which requires the name of an installed screen.

    "},{"location":"guide/screens/#pop-screen","title":"Pop screen","text":"

    The pop_screen method removes the top-most screen from the stack, and makes the new top screen active.

    Note

    The screen stack must always have at least one screen. If you attempt to remove the last screen, Textual will raise a ScreenStackError exception.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

    When you pop a screen it will be removed and deleted unless it has been installed or there is another copy of the screen on the stack.

    "},{"location":"guide/screens/#action_1","title":"Action","text":"

    You can also pop screens with the \"app.pop_screen\" action.

    "},{"location":"guide/screens/#switch-screen","title":"Switch screen","text":"

    The switch_screen method replaces the top of the stack with a new screen.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXG1T20hcdTAwMTL+nl9BcV/2qsLsTPe8btXVXHUwMDE1XHUwMDAxNlx1MDAwMVx1MDAxMpNccuH17opcdTAwMTK2bGsxsrFlXGZs5b9fjyCWLL9gg3DIKlXY1sjj1szTTz/dM8pfb1ZWVpPbTrj628pqeFNcclpRrVx1MDAxYlxmVt/689dht1x1MDAxN7VjaoL0c6/d71bTK5tJ0un99uuvl0H3XCJMOq2gXHUwMDFhsuuo11x1MDAwZlq9pF+L2qzavvw1SsLL3r/930pwXHUwMDE5/qvTvqwlXZb9yFpYi5J29/63wlZ4XHUwMDE5xkmPev9cdTAwMGZ9Xln5K/2bs65cdTAwMTZcdTAwMDWX7biWXp425MwzUDxbacepqWiElFxiKrsg6m3SjyVhjVrrZHCYtfhTq7fHrZPex6uT643+2ra4qrq7KKlmv1qPWq395LaV2tRr061kbb2k275cYo+iWtKkVlE4P+1b3Xa/0YzDXm/kO+1OUI2SWzqn+PBkXHUwMDEwN9IusjM39GlccpVgxmgrpOJSKG3VsN13IDUyY41RXHUwMDAypUGJQlx1MDAxNSzbaLdoXHUwMDFlyLJ/8PTIbDtcdTAwMGaqXHUwMDE3XHIyMK5l11xiXHUwMDE1XHUwMDA05/XsmsHD/UqnmLRcdTAwMTJN1n0zjFx1MDAxYc3Ez5DVzFx1MDAxYcFdvrVcdTAwMTemk1x1MDAwMCA5OpR22OB/sbNdS8Hwv+IoNoNu52G0Vnv+Q85ab+hWXHUwMDExSXk05eY5TCpR2N89QLt7uub0detcZuKPw75GoFx1MDAxN3S77cHqsOXbw7vMtH6nXHUwMDE23CNKaC2tdU5cIupsMltRfEGNcb/Vys61q1x1MDAxN1x1MDAxOVxi07Pf3i5cZn2Jalx1MDAxYfSFXHUwMDExllx1MDAxYm5cdTAwMTTOjf0/wi+wKW/OKp9cdTAwMGU+XHUwMDFjXHLuTreCne0vP1x1MDAxMvuCP1x1MDAwZX5pmDJGguZcXKAwelx1MDAwNPvogKHlXHUwMDFjXGJfTlx1MDAxYsefh/16cM65Klx1MDAxMftkmHBOw5LB/2Vf7Z00vsRna/31w62d/atccn5cdTAwMTmUXHUwMDAyfsdRcO2kKVx1MDAwYvxJeJNMQr6yelx1MDAxYfKNdMid4PNcdTAwMDM/6H95t3n2Z3vzc3x4dHP8YaNcIlx1MDAwZVx1MDAwZl478J1hloNBXHUwMDAx3FhrR4FvhGJIzi+ERUW+XHUwMDAxz8K9cYrXYVx1MDAxY/eC23HAXHUwMDFiUYS5UVxuLU1cdCxcdTAwMTflL0fxXHUwMDFl5dKAxlx1MDAwNVCeoalcdTAwMWQn+9FdmHLDyNnfg8uodTtcdTAwMDKJXHUwMDE0/mTgfrVcdTAwMWKG8Yr4b/xLM6rVwvif+Vx1MDAxOeuF9Pu+Qz36zfVW1PDOstpcbuujXpREpMOGzUk7N8ZVsiSg7rrbteJcdTAwMWS1u1EjioPW1+lWPc2ZXHUwMDAxi2eHYVxmOEVUoXF+bz4wx1f609ZBv9M53ZKbXHUwMDFmq+8/71x1MDAxZL52b7aOkXJDq1BobnKKNlxyYyiYoJNaodNKKlMwrFx1MDAxY29cdTAwMDbMOGTozblzXHUwMDBm3uxcZqCRSmZ38DeIWUpQzFi2N8PKL5Q2ReetcLI3g1x1MDAxYfnmkrw5b9VMb75cdTAwMWbmXHTuLCjgTPVnXHJcdTAwMTSXtMuJocf8efbML+DPxSD4gv5cZsYxdEpLZVx1MDAwNXJNideoQ2vrm41cdTAwMTDg6FJcdTAwMTC6YFo5XHUwMDFlrYk2hFBcdTAwMTKApsQ6nvHG0L9cdTAwMWRnXHUwMDE20GpDjsCdmaRRSVxccKKdJ0Tv1MxcdTAwMTn+PsMjlbJOLKJcInN2XHUwMDA03eRdXHUwMDE016K4QY1cdTAwMTmVfK8ybM9cdTAwMTElUlx1MDAxZq72vZWcXHTllPSJsybcWpVcdEs/XHUwMDE2QcdcdTAwMWLNQFhJk01aXmr/7+GKb0Ozwrj2uFGzM7CcUWucgeHcijTrp+R/kk2KQqexUjjpaO6tXHUwMDE5s6lcdTAwMTX0ko325WWU0Nh/bkdxUlx1MDAxY+N0MNe9ozfDYIxB6J7ybUVG6PhcdTAwMWVHiT17t5K5TPph+P5/bydePVx1MDAxNcv+XHUwMDE4Q3HW25v868JUZkFOT7BcdTAwMTUnW3CBPGO2XCJ9pUxmgUnUymjDSX7wQnXJSMtIXHUwMDBmWFx1MDAxNFx1MDAwMlx1MDAxMVxi/C9DZI5ZckJjuOFWTiQy5Vx1MDAxOGiN5H9cdTAwMDLISclZx5iMfIE6cFnDMojs6YnCnEQ2O3lcdTAwMWQhMs0taUhKXHRBccvR5i6651xmw1BrcjJCtsy50YIsNruGOmJcdTAwMTGXxPIkJo1cdTAwMTREU1x1MDAxM0js5+asabD1x9o4Ylx1MDAxN2StXHUwMDE5hUG0tnj2O29cdTAwMDHXXHUwMDE0yEDLjNlcdTAwMWXjrePT885e5Th+31x1MDAxZlxcRWu1TrR+dNF83Vx1MDAxOVx1MDAxNfi6oPWFXHUwMDExMFx1MDAwMimBzO72vihuXHUwMDE5+aZwzlx1MDAxMYNbmUsvn15cdTAwMTTXurzSoCNgSJJemdmlZVmPXHUwMDE1rrPg8nKFa6LGafjUToLBXFx6+Vx1MDAxODrXb+tcdTAwMDfBzqf1+LyyTTnCIN6CratS0VlcdTAwMGJ6zbBcXHgqYL5cXG2c4Vx1MDAwZYBcdTAwMTdcdTAwMTN+R0GXlI5cdTAwMDalwaJ2ZdStVYmVayFcdTAwMDWCUy+CzyFcdTAwMGJOqFx1MDAwMtTtYSVuXHUwMDFjbFx1MDAxY21tXZ9WKlx1MDAxZiuN3km/nCqAplx1MDAxMETiQC1cdTAwMDH9gtT4dFnprFEqP+WP0vOHs697h5+PzuqdXHUwMDAzPKq15Hn7Knnl9KzSlFx0lNdcdTAwMWFcblRu+S/twCGzaDSBTNNYQK75Kfi3UHVcIixz3caBXCI5vOxFS1x1MDAxYoZcdTAwMWb6XHUwMDFm7+5a56f6rI5JhLenfD70v53V77vbzul54/Bi1+3dbVx1MDAwZrqbUbR7Wi+h3+NrXHUwMDFi697B7mDQaZze1bu1qLr+R1x0/fbE+sGOXHUwMDFl3Fx1MDAwZWrrXCLYUdHazWkvLIdcdTAwMDW8KJDKubJYYFrJXHUwMDFi3dRcdTAwMDDo9Tl3Ml9Ee4xcdTAwMDFcdTAwMGX1p+jaXHUwMDFkh1eV3ubmn1j5uvm+rZ7CXHUwMDAwy0ssXHUwMDAxNVOCXCJcdTAwMWNcdTAwMTjKLa1Uo1x1MDAwYliCI6OMUyNpOGVcdTAwMTQ+s+ZccmDPw1x08kzpXHSppJxcdTAwMTDuaEpcdTAwMTRlTi9Q9J4lx5RxsFxiXHUwMDE0s1x1MDAxOc/K0mhcdTAwMTjZXHKgvVxmttqNXFwzLFJn+vd7kTrodFhvQJlb86yXVoZ/uX/BycXq3ELBMorVM6yb6Y/Ti9ZcdTAwMWPMNI90vvRHOkzM7ZCzma9cZod8XHRNqli6sMNcdH7kl06MeCRKzVxmSLBcXFhDI1JcdTAwMTRcdTAwMGLlOCQw37lUoFxyWIE4qdRjXHUwMDA1Q4da+1VlXHUwMDE0+dTtwVvRSikt8ieE5+dUeoBTQvlcdTAwMTRvnbPSM1vnreTrKs761X+Lylx1MDAxOcmlyVx1MDAxNVx1MDAxZVx1MDAxZVxuK4o5wf1FSFOluJFcdTAwMGZcdTAwMTdMKfWM3sVPVIKZjid/XHUwMDE0kZT19ib/+oRcdTAwMTWwnHuMhXdcdTAwMGUkLFx1MDAwNZ9f4M/WO6+TTYw0zO+70ki3i4iFXHUwMDA1MFx0TKVcdTAwMDOhTH7TVplUwpklia5IO5BzgsZcdFwi3zpcdTAwMDagUFJYXHUwMDE0xlx1MDAxYZlcdTAwMWKfXHUwMDA3Klx1MDAxMZo0XGKiWm7R2C836SetR5dNJWuCUTwgMVwiOWViXG5cdTAwMTByXHUwMDE33VOJZKTPNEpBI619reJvSiVrU1x1MDAwMeWPMSiVxiXcTS9cdTAwMTZcdTAwMTC1SePdaG4umZ3rvU4ukY5cdTAwMDaXK2JVXHUwMDFmzLRcdTAwMWRcdTAwMTUmNOSMa1x1MDAwZVx1MDAwNECjKVuXXHUwMDA1w8piXHUwMDEzpL7RXGItXHUwMDE1IV2bSUVcdTAwMDPHmfa0RmlcdTAwMDVcdTAwMDClNFx1MDAxM6SJcSgo21nyajrkPffHSlx1MDAxM6JcdTAwMTMgceaXRSjqar9vfTKlOO5cZo20X71cdTAwMWVfuv57UMpcZlD5Y1xmTlx1MDAwYnLKrJ3jZvqWO0cj75TUXHUwMDE57TxKKr/38fxz/Wj75FCfSLu373ai3dddgbSWM+VcdTAwMDR4tuZo5Gj5gSQzI6bn1ilcdTAwMDSH8nn7Z8tfXHUwMDFlks44Q1x1MDAxZbLkesSyloesnFx1MDAxZfJoSEg1gpj/mZ6dXHJcdTAwMGW7h4Ov8CFpVFx1MDAwNvWrvT+7W+9fOzolo2nXVlx1MDAwYidcdTAwMDGwXHUwMDEw8biXXCJCXHUwMDFiR6qMW8WfXHUwMDE38souj5NmJlx1MDAwNlEvUSxcdTAwMWJcdTAwMTLgXHUwMDEyq+NcdTAwMWLHXHUwMDFmZaRO63+Yg9v1rb3Eid8/XZRSbS7dpaZusJ7+iJyg8FwiXHSQXHUwMDBiXHUwMDE0t1x1MDAwME+qX9Q+bm13XHUwMDE0XHUwMDFjvW+a5ufaa5eQVvpcctbKbzJBa/Lrb2k6KvzOXHUwMDE3J8ibKFx1MDAxZlXuZfxcdCaloONcdTAwMWKshd/jrVx1MDAxNVx1MDAxN0t+XuLlcO6LY2ilWPrzXHUwMDEy+Cp3WOPzd1hLmPr4k/D79Ei32Pn3Jc6e+Ve5fOS0ZqBImVHex7WDwv5cdK2Ypmb/JIWw+efryvRnbVx1MDAxOGl30ipSS+B20sNQ1jFOXHUwMDEzXHUwMDAyXHUwMDAwXHUwMDBl1Eih68HXwVxuxV1+rW8ppeone+Oc+eDsXHUwMDEwMZJcdTAwMGZcbktJMeU3KFx1MDAxZFx0WpywXHUwMDA3XHUwMDEwaKa11ztaXHUwMDFhTjP+PVxyWnBb4mxcdTAwMTm4MrK5WlC+ZZVcdTAwMTbaot+GkD1dN7Rcblx1MDAxOKWupHGs81dKbse3fP9MmehULPujiOKsszf510VVicht8iqSXHUwMDE4aVx1MDAxMrA0vvPnoFti33aSRudT+6ZDaDpcYmrx2bspJNZcZqrNfjf88TpfkPDgQlxiIDCholxmf1ToXHUwMDFigcwh+ke+QPuc7zk8lnSDuNdcdLrkXHUwMDEwXHUwMDEztEkuxc20ybi058I/s/xcIlx1MDAwYuGztMmL7vuS4DRmkm9pT391w8v2dV7m/nBlktn0NF2SX+YperT0iSwp7Pk9evakL+TRS9zXXCItU9zHdK7AXHUwMDA3hsLzXHUwMDEyfl+LzzMoeZbGuGetos/0aFx1MDAwMaSB/Fx1MDAxNlNOXHUwMDFhSXOZ+48gsno1MFx1MDAwNc5vMjHoXHUwMDFmRCu6u0/wpXJmmdXq5/jjnOpkdqgoKFx1MDAwMUupo6LwbqSQOlx1MDAxYqJMnnCGXHUwMDE0XHUwMDE40Vx1MDAxMI+npamnqZPZu5hHXHUwMDE0XHUwMDEzd1xuaL6Ms1r6xYRxmyhqkDiRaK2jTFx1MDAwMIT6uZ/9mlx1MDAwZWV/rFx1MDAxNVE8TZ68efhcdTAwMDG/eWg/IcxccqeDYFx1MDAxZNVcdTAwMWWIPLvL1esoXHUwMDFjvJu0nzo9PEem4+mZKPT3+te3N9/+XHUwMDBmJe3LnyJ9 Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed

    Like pop_screen, if the screen being replaced is not installed it will be removed and deleted.

    "},{"location":"guide/screens/#action_2","title":"Action","text":"

    You can also switch screens with the \"app.switch_screen\" action which accepts the name of the screen to switch to.

    "},{"location":"guide/screens/#screen-opacity","title":"Screen opacity","text":"

    If a screen has a background color with an alpha component, then the background color will be blended with the screen beneath it. For example, if the top-most screen has a background set to rgba(0,0,255,0.5) then anywhere in the screen not occupied with a widget will display the second screen from the top, tinted with 50% blue.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVZ2VLjRlx1MDAxNH3nK1x1MDAxY+dlUlx1MDAwNZrel0mlUixhwizMXHUwMDAyXHUwMDE0Q5KplLDatsay5EhtMDPFv+dKXHUwMDE4tySvgJmFXHUwMDA3Y/dtt666zzn3XFz5y0aj0bRXXHUwMDAz03zWaJpRy4/CIPUvm5v5+IVJszCJIUSKz1kyTFvFzK61g+zZ06d9P+1cdTAwMTk7iPyW8S7CbOhHmVx1MDAxZFx1MDAwNmHitZL+09CafvZ7/nro981vg6RcdTAwMWbY1HNcdTAwMTfZMkFok/TmWiYyfVx1MDAxM9tcZlb/XHUwMDFiPjdcdTAwMWFfitdSdkHo95M4KKZcdTAwMTeBUnqK1kdcdTAwMGaTuEiVIEJcdTAwMTTmXHUwMDEyqcmMMNuDq1lcdTAwMTNAuFxyXHUwMDE5XHUwMDFiXHUwMDE3yYeaXHUwMDA3e6fPj7blaKsjRm/7r+WLbo+G7rLtMIqO7FVUJJUlcC8ultk06ZnTMLBdiOLa+Lxvpcmw041NllW+k1xm/FZor2CMo8mgXHUwMDFmd4ol3MhcYj5RwT2EhMKaXG4tKGVuO/LvXHUwMDBirDzKidKEUU0x4bW8dpNcYo5cdTAwMDHy+lx1MDAxOVx1MDAxNX8us3O/1etAenHg5mByrpRwcy7Hd8s095hiVLrluybsdG1xQMJTXHUwMDEyI12OZqY4XHUwMDAyrFx1MDAwNOFcYiniTii/5OAgKMDwsbxNcTDepnhcdTAwMThFLss88EdcdTAwMWRAZVx1MDAxMJVO99S+XHUwMDFh7n/OaHv44cxcdTAwMGbRXHQjvc7u5G4qiPPTNLlsTlwi1+N3LqPhIPBvcISF1Eoohlx1MDAxNEVu86Mw7tWTjZJWz0GvXHUwMDE4vd6cjXhrRnZcdTAwMTbctVx1MDAxNvPgzpXmikuyMti3tzvmg1x1MDAxY52/39k7XGbOXrB9/+jlm29cdHa1XGbskjOPYlx1MDAwZZCRhGvEZVx1MDAwNetKY48qXHUwMDA0jGBYYYJcdTAwMWaGdak5apNprONcdTAwMTIlJyDnuFx1MDAwZW2CqdSSUcF+cGhrJCnnWsg7QNthKIntUfj5Ro0ro/t+P4yuKkAoMFx1MDAwZlx07viZaWSt1Jj4n/jJwE9t6Edccqgx4XlkfimfWmYgl3zxkurlq2xHYSdnSzMy7SqNbFxiJWhcdTAwMTK2ycBFW5CVXHUwMDBmy6VcdTAwMDdB/e6SNOyEsVx1MDAxZlx1MDAxZK+W4UJm32z/XGZqY4llffiW21hJhDSidHV2L0bEXHUwMDFk2E1q4/dlN0ZL6S2FR6FqM62k4sLxt2C3VFDoXHUwMDE051goweRcdTAwMDMr2Vxcdlx1MDAwYk8rXHUwMDAypUpxRFx1MDAxMSup6YTrjIDKQFx1MDAwZZAoQpxxR/Ax9TlVipOy7VjOfEfpW6CQ8cj1fEFYUI1cdTAwMDT8x+w+lM0swHknjIMw7lRcdTAwMTNcdTAwMWL7tINcdTAwMTWKR0Hy1jDPclx1MDAwYnmMXHUwMDExTjCWXHUwMDFja02k5LQ0reNcdTAwMGbyrKlHJYg2lVx1MDAxNFx1MDAwYqZcdTAwMTGmU3dv4mB5Vov9Wy0rSYhcdTAwMDY0gUeUXHUwMDA03rNZWYGMc5goXHUwMDE54pzK6TOJ/MzuJv1+aGH73yZhbOvbXFzs53ZO+q7xp5RcdTAwMDXuqlx1MDAxY6urwyBfsSr+7l3D0af4MHn/cXPm7K254C6iU7h2622U/89cdTAwMTO2XHUwMDA1Jl2XqmDdpGMmKNdwXGYrK9v5q+10p3+xS+K93f1Pg97OSetcXH/nJl16YFx1MDAwZlx1MDAxNVx1MDAxMYJcdTAwMDFcdTAwMTFwzaRzykDaKEVcdTAwMWPioHO0ltfdpC2f0W6vz6QrTYG1uFR6XHUwMDFl08hkx1x1MDAwN0efgyx5d3T4qtc+XHUwMDE57tn/9vl6jFxmQUJAidGP7dFcdTAwMDWei3aMXHUwMDE1dDtSs9XR3rvAXHUwMDA3Q7/bZfzy9fut5+jw5cuT/lx1MDAxY7R3/VZ3mJrHxrtehnfCpKdcdTAwMTDCQlMmudBYVfDOoJTDXHUwMDE0qMFaXHUwMDAx9Vx1MDAxMWNcdTAwMGZcdTAwMDG8Tf04XHUwMDAzXHUwMDBmXHUwMDA26JpcdTAwMDY90WpcdTAwMWHt025cdTAwMWS6XHUwMDA1XHKtk6Lix1x1MDAwNzlXuX5/Nbd+nFxmtvpJZid+2Fx1MDAxZNGzRto595+gTbRJON9EXHUwMDFl/+XX78G+3zXl+/l5XVLMulxmXHUwMDEwiolAtESkZTKwXHUwMDE4MneSga9n6Fx1MDAxOZdcdTAwMWU4p7yJXHUwMDE0WKmyaS/qXHUwMDFlo56kXGaD+6JKXHUwMDEzVU9sfTJQ6Fx1MDAxMaRcdTAwMDCSxEB+S1x1MDAwZlx1MDAwZZytp55ASjFcZonkRdopwa1GIIrh4PSdXG7hWn19QW7KXHUwMDFm1dcvLjdVXHUwMDA3jYnWmGHwiphLrkt28tZAM3BcdTAwMWOC5s9cdTAwMWQpwlJSdT9bv9jxVZNC4HBcdTAwMTTO5VxcXG4o9nQ6K+VJXHUwMDAxglx1MDAwZolcdTAwMDMuOViCXHUwMDFm2tXPhXZcdTAwMTGsg/qOnr7Q51nahuY+hsR5h1x1MDAwMVxyk1hd20YjLsy74+fxv+/SreNR/81W/Fx1MDAxN/qWhn65slx0rj2Ru3VcZrZcdTAwMWSUy1GyeOqOmCeIgK6RsvyZ7MNcZn17lpsnTHlcdTAwMWFLaNhcdTAwMDDo4M/pXGafXHUwMDAzzPTAYyEmQHkxNFx1MDAxN072bjVccu6UcSRLT15cdTAwMWbqe5Y9XCJfh3jVyTYnsmZcdTAwMWFXYuvuzFn+qIFyUYNT/lx1MDAwN4XHQ0RcdTAwMTM4KyrhMMXS5SSUXZDbvJ/UnGJV0YRpVCxbTzFcdTAwMGauyzFcdTAwMDHTLlx1MDAxONOV5Th4JFx1MDAwNEJKsZZMwuJs2XLz9mYlRZrbdPFcdTAwMDVuXHUwMDBieMpcdTAwMDCBqyuSPbeH5oCEn9jhi1x1MDAxM3N0XHUwMDE2nqG389zWN1Mk7oHOglx1MDAwMHFcIjlUV+S+lytcdTAwMTRcdTAwMTfYg6BWgFx1MDAxYlx1MDAwNWfmXHUwMDA0u1AoXHUwMDAxVVx1MDAxMLxcZlVcdTAwMWO8XHUwMDE3zF2/QuGST3GS5LrvsVx1MDAwNFx0XHUwMDAyXaBcdTAwMDJ3+OhcbpT/kiFcdFbuXu/XXHUwMDFilaxhpTeqNjH5xvxpoihpnCZpXHUwMDE0/DSz8Sn9RvU1XHUwMDFhn0o+NzTbXHUwMDE407TpXHUwMDBmXHUwMDA2R1x1MDAxNnZrYsPgXHUwMDFjwmB8y27V5kVoLndmYyCHwcaYujlHTOGArzeu/1x1MDAwN0PH3J0ifQ== Base screen(partial visible)Top-most screenbackground: rgba(0,0,255,0.5);Hello World!

    Note

    Although parts of other screens may be made visible with background alpha, only the top-most is active (can respond to mouse and keyboard).

    One use of background alpha is to style modal dialogs (see below).

    "},{"location":"guide/screens/#modal-screens","title":"Modal screens","text":"

    Screens may be used to create modal dialogs, where the main interface is temporarily disabled (but still visible) while the user is entering information.

    The following example pushes a screen when you hit the Q key to ask you if you really want to quit. From the quit screen you can click either Quit to exit the app immediately, or Cancel to dismiss the screen and return to the main screen.

    OutputOutput (after pressing Q)modal01.pymodal01.tcss

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    ModalApp \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2588 \u2588\u2588 \u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588 \u2588QuitCancel\u2588 \u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588

    modal01.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(Screen):\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.app.exit()\n        else:\n            self.app.pop_screen()\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        self.push_screen(QuitScreen())\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    Note the request_quit action in the app which pushes a new instance of QuitScreen. This makes the quit screen active. If you click Cancel, the quit screen calls pop_screen to return the default screen. This also removes and deletes the QuitScreen object.

    There are two flaws with this modal screen, which we can fix in the same way.

    The first flaw is that the app adds a new quit screen every time you press Q, even when the quit screen is still visible. Consequently if you press Q three times, you will have to click Cancel three times to get back to the main screen. This is because bindings defined on App are always checked, and we call push_screen for every press of Q.

    The second flaw is that the modal dialog doesn't look modal. There is no indication that the main interface is still there, waiting to become active again.

    We can solve both those issues by replacing our use of Screen with ModalScreen. This screen sub-class will prevent key bindings on the app from being processed. It also sets a background with a little alpha to allow the previous screen to show through.

    Let's see what happens when we use ModalScreen.

    OutputOutput (after pressing Q)modal02.pymodal01.tcss

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0i\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0\u2588\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u2588 Fear\u00a0is\u00a0th\u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 I\u00a0will\u00a0fac\u2588\u2588 I\u00a0will\u00a0per\u2588\u2588\u2585\u2585 And\u00a0when\u00a0i\u2588\u2588 Where\u00a0the\u00a0\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588QuitCancel\u2588 Fear\u00a0is\u00a0th\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0fac\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    modal02.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(ModalScreen):\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.app.exit()\n        else:\n            self.app.pop_screen()\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        \"\"\"Action to display the quit dialog.\"\"\"\n        self.push_screen(QuitScreen())\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    Now when we press Q, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input.

    "},{"location":"guide/screens/#returning-data-from-screens","title":"Returning data from screens","text":"

    It is a common requirement for screens to be able to return data. For instance, you may want a screen to show a dialog and have the result of that dialog processed after the screen has been popped.

    To return data from a screen, call dismiss() on the screen with the data you wish to return. This will pop the screen and invoke a callback set when the screen was pushed (with push_screen).

    Let's modify the previous example to use dismiss rather than an explicit pop_screen.

    modal03.pymodal01.tcss modal03.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(ModalScreen[bool]):  # (1)!\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.dismiss(True)\n        else:\n            self.dismiss(False)\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        \"\"\"Action to display the quit dialog.\"\"\"\n\n        def check_quit(quit: bool) -> None:\n            \"\"\"Called when QuitScreen is dismissed.\"\"\"\n            if quit:\n                self.exit()\n\n        self.push_screen(QuitScreen(), check_quit)\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    1. See below for an explanation of the [bool]
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    In the on_button_pressed message handler we call dismiss with a boolean that indicates if the user has chosen to quit the app. This boolean is passed to the check_quit function we provided when QuitScreen was pushed.

    Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. This makes it easier for the app to perform any cleanup actions prior to exiting, for example.

    Returning data in this way can help keep your code manageable by making it easy to re-use your Screen classes in other contexts.

    "},{"location":"guide/screens/#typing-screen-results","title":"Typing screen results","text":"

    You may have noticed in the previous example that we changed the base class to ModalScreen[bool]. The addition of [bool] adds typing information that tells the type checker to expect a boolean in the call to dismiss, and that any callback set in push_screen should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs.

    "},{"location":"guide/screens/#waiting-for-screens","title":"Waiting for screens","text":"

    It is also possible to wait on a screen to be dismissed, which can feel like a more natural way of expressing logic than a callback. The push_screen_wait() method will push a screen and wait for its result (the value from Screen.dismiss()).

    This can only be done from a worker, so that waiting for the screen doesn't prevent your app from updating.

    Let's look at an example that uses push_screen_wait to ask a question and waits for the user to reply by clicking a button.

    questions01.pyquestions01.tcssOutput questions01.py
    from textual import on, work\nfrom textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Label\n\n\nclass QuestionScreen(Screen[bool]):\n    \"\"\"Screen with a parameter.\"\"\"\n\n    def __init__(self, question: str) -> None:\n        self.question = question\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.question)\n        yield Button(\"Yes\", id=\"yes\", variant=\"success\")\n        yield Button(\"No\", id=\"no\")\n\n    @on(Button.Pressed, \"#yes\")\n    def handle_yes(self) -> None:\n        self.dismiss(True)  # (1)!\n\n    @on(Button.Pressed, \"#no\")\n    def handle_no(self) -> None:\n        self.dismiss(False)  # (2)!\n\n\nclass QuestionsApp(App):\n    \"\"\"Demonstrates wait_for_dismiss\"\"\"\n\n    CSS_PATH = \"questions01.tcss\"\n\n    @work  # (3)!\n    async def on_mount(self) -> None:\n        if await self.push_screen_wait(  # (4)!\n            QuestionScreen(\"Do you like Textual?\"),\n        ):\n            self.notify(\"Good answer!\")\n        else:\n            self.notify(\":-(\", severity=\"error\")\n\n\nif __name__ == \"__main__\":\n    app = QuestionsApp()\n    app.run()\n
    1. Dismiss with True when pressing the Yes button.
    2. Dismiss with False when pressing the No button.
    3. The work decorator will make this method run in a worker (background task).
    4. Will return a result when the user clicks one of the buttons.
    questions01.tcss
    QuestionScreen {\n    layout: grid;\n    grid-size: 2 2;                \n    align: center bottom;\n}\n\nQuestionScreen > Label {\n    margin: 1;       \n    text-align: center;\n    column-span: 2;    \n    width: 1fr;\n}\n\nQuestionScreen Button {\n    margin: 2; \n    width: 1fr;     \n}\n

    QuestionsApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Do\u00a0you\u00a0like\u00a0Textual?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    The mount handler on the app is decorated with @work, which makes the code run in a worker (background task). In the mount handler we push the screen with the push_screen_wait. When the user presses one of the buttons, the screen calls dismiss() with either True or False. This value is then returned from the push_screen_wait method in the mount handler.

    "},{"location":"guide/screens/#modes","title":"Modes","text":"

    Some apps may benefit from having multiple screen stacks, rather than just one. Consider an app with a dashboard screen, a settings screen, and a help screen. These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. But we may still want each individual screen to have a navigation stack where we can push and pop screens.

    In Textual we can manage this with modes. A mode is simply a named screen stack, which we can switch between as required. When we switch modes, the topmost screen in the new mode becomes the active visible screen.

    The following diagram illustrates such an app with modes. On startup the app switches to the \"dashboard\" mode which makes the top of the stack visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 \"dashboard\"\"help\"\"settings\"Active (visible)

    If we later change the mode to \"settings\", the top of that mode's screen stack becomes visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== \"dashboard\"\"help\"\"settings\"Active

    To add modes to your app, define a MODES class variable in your App class which should be a dict that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. However you specify it, the values in MODES set the base screen for each mode's screen stack.

    You can switch between these screens at any time by calling App.switch_mode. When you switch to a new mode, the topmost screen in the new stack becomes visible. Any calls to App.push_screen or App.pop_screen will affect only the active mode.

    Let's look at an example with modes:

    modes01.pyOutputOutput (after pressing S)
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Footer, Placeholder\n\n\nclass DashboardScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Dashboard Screen\")\n        yield Footer()\n\n\nclass SettingsScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Settings Screen\")\n        yield Footer()\n\n\nclass HelpScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Help Screen\")\n        yield Footer()\n\n\nclass ModesApp(App):\n    BINDINGS = [\n        (\"d\", \"switch_mode('dashboard')\", \"Dashboard\"),  # (1)!\n        (\"s\", \"switch_mode('settings')\", \"Settings\"),\n        (\"h\", \"switch_mode('help')\", \"Help\"),\n    ]\n    MODES = {\n        \"dashboard\": DashboardScreen,  # (2)!\n        \"settings\": SettingsScreen,\n        \"help\": HelpScreen,\n    }\n\n    def on_mount(self) -> None:\n        self.switch_mode(\"dashboard\")  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = ModesApp()\n    app.run()\n
    1. switch_mode is a builtin action to switch modes.
    2. Associates DashboardScreen with the name \"dashboard\".
    3. Switches to the dashboard mode.

    ModesApp Dashboard\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

    ModesApp Settings\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

    Here we have defined three screens. One for a dashboard, one for settings, and one for help. We've bound keys to each of these screens, so the user can switch between the screens.

    Pressing D, S, or H switches between these modes.

    "},{"location":"guide/styles/","title":"Styles","text":"

    In this chapter we will explore how you can apply styles to your application to create beautiful user interfaces.

    "},{"location":"guide/styles/#styles-object","title":"Styles object","text":"

    Every Textual widget class provides a styles object which contains a number of attributes. These attributes tell Textual how the widget should be displayed. Setting any of these attributes will update the screen accordingly.

    Note

    These docs use the term screen to describe the contents of the terminal, which will typically be a window on your desktop.

    Let's look at a simple example which sets styles on screen (a special widget that represents the screen).

    screen.py
    from textual.app import App\n\n\nclass ScreenApp(App):\n    def on_mount(self) -> None:\n        self.screen.styles.background = \"darkblue\"\n        self.screen.styles.border = (\"heavy\", \"white\")\n\n\nif __name__ == \"__main__\":\n    app = ScreenApp()\n    app.run()\n

    The first line sets the background style to \"darkblue\" which will change the background color to dark blue. There are a few other ways of setting color which we will explore later.

    The second line sets border to a tuple of (\"heavy\", \"white\") which tells Textual to draw a white border with a style of \"heavy\". Running this code will show the following:

    ScreenApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    "},{"location":"guide/styles/#styling-widgets","title":"Styling widgets","text":"

    Setting styles on screen is useful, but to create most user interfaces we will also need to apply styles to other widgets.

    The following example adds a static widget which we will apply some styles to:

    widget.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass WidgetApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(\"Textual\")\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.border = (\"heavy\", \"white\")\n\n\nif __name__ == \"__main__\":\n    app = WidgetApp()\n    app.run()\n

    The compose method stores a reference to the widget before yielding it. In the mount handler we use that reference to set the same styles on the widget as we did for the screen example. Here is the result:

    WidgetApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Textual\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Widgets will occupy the full width of their container and as many lines as required to fit in the vertical direction.

    Note how the combined height of the widget is three rows in the terminal. This is because a border adds two rows (and two columns). If you were to remove the line that sets the border style, the widget would occupy a single row.

    Information

    Widgets will wrap text by default. If you were to replace \"Textual\" with a long paragraph of text, the widget will expand downwards to fit.

    "},{"location":"guide/styles/#colors","title":"Colors","text":"

    There are a number of style attributes which accept colors. The most commonly used are color which sets the default color of text on a widget, and background which sets the background color (beneath the text).

    You can set a color value to one of a number of pre-defined color constants, such as \"crimson\", \"lime\", and \"palegreen\". You can find a full list in the Color API.

    Here's how you would set the screen background to lime:

    self.screen.styles.background = \"lime\"\n

    In addition to color names, you can also use any of the following ways of expressing a color:

    • RGB hex colors starts with a # followed by three pairs of one or two hex digits; one for the red, green, and blue color components. For example, #f00 is an intense red color, and #9932CC is dark orchid.
    • RGB decimal color start with rgb followed by a tuple of three numbers in the range 0 to 255. For example rgb(255,0,0) is intense red, and rgb(153,50,204) is dark orchid.
    • HSL colors start with hsl followed by a angle between 0 and 360 and two percentage values, representing Hue, Saturation and Lightness. For example hsl(0,100%,50%) is intense red and hsl(280,60%,49%) is dark orchid.

    The background and color styles also accept a Color object which can be used to create colors dynamically.

    The following example adds three widgets and sets their color styles.

    colors01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Static\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(\"Textual One\")\n        yield self.widget1\n        self.widget2 = Static(\"Textual Two\")\n        yield self.widget2\n        self.widget3 = Static(\"Textual Three\")\n        yield self.widget3\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"#9932CC\"\n        self.widget2.styles.background = \"hsl(150,42.9%,49.4%)\"\n        self.widget2.styles.color = \"blue\"\n        self.widget3.styles.background = Color(191, 78, 96)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    Here is the output:

    ColorApp Textual\u00a0One Textual\u00a0Two Textual\u00a0Three

    "},{"location":"guide/styles/#alpha","title":"Alpha","text":"

    Textual represents color internally as a tuple of three values for the red, green, and blue components.

    Textual supports a common fourth value called alpha which can make a color translucent. If you set alpha on a background color, Textual will blend the background with the color beneath it. If you set alpha on the text color, then Textual will blend the text with the background color.

    There are a few ways you can set alpha on a color in Textual.

    • You can set the alpha value of a color by adding a fourth digit or pair of digits to a hex color. The extra digits form an alpha component which ranges from 0 for completely transparent to 255 (completely opaque). Any value between 0 and 255 will be translucent. For example \"#9932CC7f\" is a dark orchid which is roughly 50% translucent.
    • You can also set alpha with the rgba format, which is identical to rgb with the additional of a fourth value that should be between 0 and 1, where 0 is invisible and 1 is opaque. For example \"rgba(192,78,96,0.5)\".
    • You can add the a parameter on a Color object. For example Color(192, 78, 96, a=0.5) creates a translucent dark orchid.

    The following example shows what happens when you set alpha on background colors:

    colors01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Static\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        self.widgets = [Static(\"\") for n in range(10)]\n        yield from self.widgets\n\n    def on_mount(self) -> None:\n        for index, widget in enumerate(self.widgets, 1):\n            alpha = index * 0.1\n            widget.update(f\"alpha={alpha:.1f}\")\n            widget.styles.background = Color(191, 78, 96, a=alpha)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    Notice that at an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.

    ColorApp alpha=0.1 alpha=0.2 alpha=0.3 alpha=0.4 alpha=0.5 alpha=0.6 alpha=0.7 alpha=0.8 alpha=0.9 alpha=1.0

    "},{"location":"guide/styles/#dimensions","title":"Dimensions","text":"

    Widgets occupy a rectangular region of the screen, which may be as small as a single character or as large as the screen (potentially larger if scrolling is enabled).

    "},{"location":"guide/styles/#box-model","title":"Box Model","text":"

    The following styles influence the dimensions of a widget.

    • width and height define the size of the widget.
    • padding adds optional space around the content area.
    • border draws an optional rectangular border around the padding and the content area.

    Additionally, the margin style adds space around a widget's border, which isn't technically part of the widget, but provides visual separation between widgets.

    Together these styles compose the widget's box model. The following diagram shows how these settings are combined:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT2txcdTAwMTb+3l/h+H4tu/t+6cyZM9VqrbfWevfMO51cYlx1MDAwMaJAaFx1MDAxMlx1MDAxNHyn//2sXHUwMDFklCSERFCw6Dn50EpcdTAwMTKSxd7redaz1r78825lZTVcdTAwMWF03dWPK6tuv+q0vFrg3K6+t+dv3CD0/Fx1MDAwZVxcovHn0O9cdTAwMDXV+M5mXHUwMDE0dcOPXHUwMDFmPrSd4NqNui2n6qJcdTAwMWIv7DmtMOrVPFx1MDAxZlX99lx1MDAwNy9y2+G/7b/7Ttv9V9dv16JcdTAwMDAlL6m4NS/yg+G73JbbdjtRXGJP/1x1MDAwZnxeWfkn/jdlXeBWI6fTaLnxXHUwMDE34kuJgZzT8bP7fic2llBFleCck9FcdTAwMWRe+Fx1MDAxOd5cdTAwMTe5NbhcXFx1MDAwN5vd5Io9tTo46Tt6Y//TbvubXHUwMDFmnISHXHUwMDFkv16tJa+te63WYTRoXHKbwqk2e0HKqDBcbvxr99SrRU379rHzo+/V/MhcdTAwMWEwulx1MDAxY/i9RrPjhmHmS37XqXrRwJ7DeHR22FxmXHUwMDFmV5IzfdtcdTAwMDZYXCIhpMGYXHUwMDEwybA0cnQ5flx1MDAwMMdIY2FcYiZGKEFcdTAwMTVcdTAwMWYzbd1vQW+AaX/h+Ehsu3Sq11xyMLBTXHUwMDFi3Vx1MDAxM1x1MDAwNU4n7DpcdTAwMDH0WXLf7f2PXHUwMDE22CCmNJVUXHUwMDBiplxyS35P0/VcdTAwMWHNyFx1MDAxYUtcdMJaXHUwMDE4psTwbYmxoVx1MDAxYndcZtjJXGI8JfmR1oTu11rsI3+Pt2vTXHS69823XHUwMDFh2lx1MDAwZinzreVcdTAwMWLjXHUwMDBllnayVN9/r+v+vt5vXa5HV0dH52dVXHUwMDE11ndGz8p4pFx1MDAxM1x1MDAwNP7t6ujK7/u/XHUwMDEy03rdmjP0MlwiJSOSSo0l06PrLa9zXHJcdTAwMTc7vVYrOedXr1x1MDAxM8eMz/5+/1x1MDAwNERIZVxuXHUwMDExYYji4Fx1MDAwNFRNjYjdra+tur9f3aq3XHUwMDA3P1x1MDAxYd7N7lx1MDAwMd1cZlx1MDAwYlx1MDAxMFx1MDAxMfqA75nxMPatx+DAXHUwMDFlRYNcdTAwMDI0XHUwMDE4TVx1MDAxOVx1MDAxNVx1MDAxYyshWFx1MDAxNlxylGokJSZKXHS4R6cvj6NB1FmtykvR8Fx1MDAxN69Kty7ySGBCIS1cdTAwMDSXWok8XGKoMIhcdTAwMWFcdTAwMDNewVx1MDAxOcNU5EFAXHUwMDE5XHUwMDEzklxirl9cdTAwMTZcdTAwMDTVb7vnzbOt49rAmE5vn3RVs+a9Qlx1MDAxMHBZXGZcdTAwMDImMeVUSjE1XGKud6+u2mHw6zRw+4HZONu46PHPT1x1MDAwYlx1MDAwYrRcYlx1MDAwNjUnbM43LFxiRpFSXFxIYaiSWpAsXHUwMDBlXHUwMDE0uKDWWlx1MDAxOUOoIIqwQlx1MDAxY7hSqedEXHUwMDA18O88XHUwMDA0SMq1XHUwMDFmiJ9pXGJh0DX8pZz+wZdcIrdcdTAwMWZlvXzY8TtcdTAwMTef9k5+bW1drG1dXHUwMDA0P2818/prTsrn309+7PDLd1x1MDAwM7355Yiz3Z1+c933mt+rXHUwMDFi11+WXHUwMDEzS5nfn5Z/KZCMwchIQjhcdTAwMDeKmlx1MDAxYUU3XHUwMDFlZnzT7Kvewefq9aU52D6W3+YsrmZcZiaPg0hcdTAwMWGDuLA/lEpmtKFcdTAwMTlcdTAwMTBcdMKQ1pJcdTAwMTgjtNRcdTAwMTBNXHUwMDE2pqwkzUOIilx1MDAxY4KwkFQxiHHzR9A8nTHpdL9cdTAwMTNcdTAwMWR6d7bdKc6c3XTaXmuQ6bfYS8HSPSdoeJ10W4YuvDMmd525+1PLa1g/Xm259ayDR1x1MDAxZWQjo8uRn/rlVXi7XHUwMDAzj1x1MDAwYr7Wxn+FXHUwMDFmePBmp3WUteRJ2GKiXHUwMDEwW1x1MDAwNGSaUcDabGpwXHLCzs7hQe1qw2l+3dq9lp2Dm7D5gplcdTAwMGJ+XCK6IERRK4K0XHLLXFxk0Fx1MDAwNalcdTAwMWJcdTAwMDLgYSpccmRcdTAwMTOMXHUwMDFhtTB4pXKicngpSFxcXGZ+YVXmqeb2TXV7/etGh31yu+etSPw8mmskSdTSosH73anVvE5jXHUwMDE50PtgytNCI1bjZ1x1MDAxZuBcdTAwMGLaXHUwMDFkXHUwMDEyXFwpplx1MDAwZo2Tdcayo1cwVVwiMFx1MDAxOSdIvZDA5Fx1MDAxM1x1MDAwNCZNvW9cYl9gXHUwMDFiyLYgUL98cFxcXHUwMDE0vlhcdTAwMGVf63BcdTAwMTmsWoGmciaDzExcdTAwMDZZXHUwMDE1vuVcdTAwMDYlMGt7tVo628pcIu2xJGlcdTAwMWN8XHUwMDE5O0tcdTAwMTFYnuhpXlx1MDAwNEPCpWCCSjJ9tWPnR6O5dbm5b1x1MDAwNs2rk8b+USDO+rdcdTAwMDU4rFx1MDAwNn5cdTAwMThWmk5UbVx1MDAxNmFxvNC2OJlcdTAwMWFcdTAwMTc9tFx1MDAxMYRgI1xyIYxnsEipRJBZXHRNjZREM1lcXFx1MDAwMpyi6FGKxcdcdTAwMGJcdTAwMWaGcCzzwVVcYlxyvVx1MDAwNVx1MDAwMvZlY+vF55/O2t2nauv48MugvXf16cfBXHUwMDE1ny62lmZ/e9v9XHLj17c3RXBUOe+37rw9df3HYnYpwIbvn1x1MDAwNC4qcVx1MDAxMbooMUopkGVTg6u8pWdcdTAwMDZXYSVl7uBcdTAwMTJGIU4heGCNtVx1MDAwMcfO5oBcdTAwMTSucmhcdGIgXHUwMDAzNGRxOSAjiHJcYqZcdTAwMWNiKSeQdubxxTRcdTAwMTJcdTAwMDRMZFx1MDAxNFBuMOXjKCNYWv+RqXxyapjFpr50XHUwMDEwXGYjJ4jWvE6s1D6mkPYwcjSMPj0xOOmJdXzt4cPTtmxf3ahcdTAwMTM/XHUwMDA1N1xim9Ve7Fx1MDAwMlxiY26w4Fx1MDAxYfqCXHUwMDFhbFL3NJxu3ESIKMNcdTAwMTVcdTAwMTAp3Mah3+/vXHUwMDE4XHUwMDAxftXt1Fx1MDAxZTepPJikTKpgRDU3jFxi8DBwMS1VziiKjFx1MDAwMXMgXHSC+5SUQuWMajlhtO63257Ved99r1x1MDAxM403cdyWnyzam66Tk8fwo9LXxmmha5+YpdPkr5VcdTAwMDQy8YfR33+/n3h3oSvbo5Lz4uRx79L/z6rZ7buK+Ixhrlx1MDAxOZMzpNzlLvdcInz2RN1O49ZXoFx1MDAxNojmNFW5XHUwMDE51rQ4XHUwMDAyNU9cdTAwMTlIXHUwMDA1cDSlx+yaY00rXHT1JUm35sImXHUwMDEwL1dcdTAwMTR+ti5YQPyeJSfI59xrflBLS/s/l3LfW/I0OWL5sVxivuCwilE9vdQv12fzXHUwMDE505k7cpUmXGJcdTAwMTRcYoGMmyvKx1Q+5DqIgFx1MDAxMiFcdTAwMDRcdTAwMTBDXHUwMDE0Xdw4P1x1MDAwNCwhpVxyVMDdXFzTXHTFaWB4XHUwMDA29zCOXHUwMDE52GnrezkpXCKYYoxi/lx1MDAwNGQvg1x1MDAxNFx1MDAxOVx1MDAwZp5zUFx1MDAwNMNYr5GWXHUwMDE0Q8tcdTAwMTgsQX/wtERJaVx1MDAwNoWNVkpyyZTQkHK9akFQ6FH2qOSdaUZFUMwpjJeMXHUwMDE0K2MoVnz6Ql75JJIlZlx1MDAxNWh6XHUwMDAzv5RA2/Ox/IZhXHUwMDA0XHQ7tFx1MDAxM8EgkPi4XfNkXHUwMDE1XHTv4LFcdTAwMTZcdTAwMDbP5olcZs7QitJcdTAwMTRslWCtJFxc5jNcdTAwMWNcdTAwMDHGgqh5ylx1MDAxONjrpJXyWWsp0oCO5EZcdTAwMDCSKFFcdTAwMDI4I2m8JPl55SxS5EH2yPvOjCxcdTAwMTJrplx0JKJTXHUwMDEyepxDpGBcdTAwMDY4LTUp6zFcdTAwMGVZ29w4ucWuUP0tsbl95ZxXfTlY9nFyYFxyXHUwMDA09IHt1Cqp+Vx1MDAxOIdAXHUwMDFlh1xmqFx1MDAxMiPhXHUwMDA2I1x1MDAwNF/kXHUwMDE0RESkMpPLj1x1MDAwNI1XJlx1MDAxZkhDXHUwMDEzhTGHZPOtkUb2YfPFcubaXFyBPKFcdTAwMTftMeq/OVx1MDAwMVfIYuBqJY2tqE9fXHUwMDEwYNu/blx1MDAwZqtcdTAwMDbj3unNSbOmyMHaN7b0wIWmVpBdXHUwMDFiqZSwXHUwMDAzXGJcdTAwMTngcq3tbF3OoNlcdTAwMDEgIDdcdTAwMTdcdTAwMDZcXFx1MDAwZbJXXHUwMDEwkVx1MDAxZVx1MDAxOVx1MDAxOOFcdTAwMTYjXHUwMDE2K8NcdMDlXHUwMDAwXFyRKVP8XHUwMDFmuH9cdTAwMTC4+V60RyXpwHlcdPd0oT1cdTAwMDddyCVcdTAwMTQ1eHrofu7XpXvxq7Le9DaOf7Y/fz6+/LGx9NBVXHUwMDE0KfBHyqmiXHUwMDEyXHUwMDA0XVx1MDAwNrqMcWQ4t9OIXHUwMDE1aFx1MDAxZbLIaoBRRnHGXGJjoKxcZp9Q14PcXHUwMDAxXHSMNYeeseV3kUoj7mc8cylcdTAwMTSh4pVcdTAwMDK5SJz7Z8e7J25w1T350Vx1MDAxZmzc7eytNUNZMFxugDlcdTAwMTaMQdBRiimuVaoqnoxNUFx1MDAwMklcdTAwMWFcdTAwMTNwp5mQ879cdTAwMTiFLFTHV4pdKr6c96Z50VxuKFg2fvqBV5TClNmumZpWSO07ub08qG5+ObxsrK/fnlx1MDAwN+J4b/lphSBbeLKDUYaQ8Wk9SiNlsJ3xg+2s2MVccndcdTAwMTJkbbAlXHUwMDA3zOFl6XZcdTAwMWbRiiHgKKBbiKCaKZErXHUwMDA2KEq4XHUwMDA2J3mlsv7ZpFx1MDAwMjKaQKbLJFx1MDAwNVxmYYEpyXOKRra4Q5WhmFx1MDAxMSHMXHUwMDFi5ZRid7LHuCPNyCdFI466eFx1MDAwMoWCfrEj0dOLlPJeX1Y24Vx1MDAxY0lNtDBKXGLI/rOFXHUwMDAxxlx1MDAwNbI+Z9dCcWpSq3bmXlx1MDAxN0hcdTAwMWVdMthoQDpcdTAwMTKhzVx1MDAwYk/wLY9cdTAwMTNcdTAwMTlPm2lcdTAwMTJSuawtfe6D2y+C5Z40iLk17LWUXHUwMDAz/KlBzHtLSlx0oajiYEwhIVx1MDAxMMUhrIKomX698vlp4/i6urfryruoe0hMv1x1MDAxMX3dWnZG4DZtXHUwMDExTFPQd5ZcdTAwMDGzaVx1MDAwYsVcdTAwMDYkrq38gPbA6ULiXHUwMDAyhlx1MDAxYqC9gfCp4cD8lEzQXHUwMDE3QE9cdTAwMTiDXHUwMDEw1Vx1MDAwMi5TjXW+XHUwMDAwXHUwMDAxeZfAdsnz65RcdTAwMThvrVx1MDAwMFHcq/ao5Dt0xkhfXHUwMDA07LQ8XHUwMDFkn5sgjLaZw/TDiL1m55Z4XHUwMDBl+bV5Snb5trk46vhF85CXXHUwMDA215pcdESltPPqMNCcyuZcclx1MDAxNEtEQIcyw5nGgJrFVVx1MDAxMjHSRmktXHUwMDA1SCwstZ5Uj+B2XHUwMDE3XHUwMDAyUIFK2GXimuSHXHUwMDExJaRcdTAwMGWcavXmZie8VlxcXHUwMDE3dao9Krn+nFx1MDAxMdbFXHUwMDA1gZJcdTAwMTVcdTAwMDbUzlx1MDAxMOCQXHUwMDExT4/s5vb15UG3Ujlccu+2XHUwMDBl1ytsoPYrRZOgl1x1MDAwNtmSaGR3XHUwMDE2XHUwMDAxXHUwMDE1z5igKrtMj1x1MDAxOMu5XHUwMDE4Wt72S3pa8twrXHUwMDAyTFwiXHUwMDEwXGbA21x1MDAwNOjFpEZdUyN8QENcdTAwMWOua8ZcdTAwMTRcdTAwMDVcdJFbZc4451pcdTAwMTP6SuN1UUngSNd37i62XHUwMDFiP9yzjbWdwDn9eXjzs6DOSOxcXFx1MDAwZYhJXHUwMDEyMmHDSXqyTVJntHOMsVx1MDAxMZgzm1x1MDAxMt3f8NaKXHUwMDAylUKXXHUwMDFhXs1509xoJb1mLbdxXHUwMDExwVSotPZ9jFakS25PXHUwMDBm1k7Xe7+O62FwuH6MXbr0tGIoMsDXkFx1MDAwYmBlXGZcdTAwMWKjXHUwMDE1bZBcItb9qFLMyOLlg8+mXHUwMDE1amfcXHUwMDAxfeF4jDNN+JlcdTAwMTFIwlx0RJ94sJHp9GLke8VAiMBcdTAwMDJj/r/KLFx1MDAxOMHPl1RBXG5cdTAwMGKQsqFxwvxcIoKInZwllMKEQ7fn1zG8XHJmKXYqe1Qm+NOM1FJUclQlK1wiXHK1vjLDfMbyvl9WXrFNXHUwMDBmylx1MDAxZNI7q81otuRIlERSc6OJsfNcdTAwMWQoW1x1MDAxY7GI5NFlu1xuQMakiZb6XHS88ZyiY7lcdTAwMTTN+NpMRcfyWFT63Fx1MDAwN8dfXHUwMDA02z2p6Dh03pRcdTAwMDP8qZrj0JCnaVxyXjxVgjOioEVnmJ1YvqvR0s5wZlxiYo3dQ4RplopLw0lOQiFiXGaVIDMoXHUwMDExophcdTAwMTBcdTAwMThnXHUwMDBlf1ZlXHUwMDAyXHUwMDEzO81cdTAwMTAzXGJcdTAwMGbAw8JM2nfEqlwiXHUwMDAxYcFcdTAwMTZBIUayXFxcbmOkgUeItzdZsUiClO8tMFx1MDAxMlx1MDAxN1x1MDAxMlx1MDAxOWm1uuTSjk7zlJhP0lx1MDAxZrtcdTAwMDCUXHUwMDAw30L+w6lcdTAwMDHflzlcdTAwMDXymnRGpcSn4ut5d5pRaFx1MDAxNOcwqmRcdTAwMDcjIZmgRE5PLOWb3CwtsVDEscF2koFcdTAwMTFybPKkXHUwMDA0satBXGZcdTAwMTOFXHUwMDAxPCUrKZ/PK5rH0/ol1liI9JKqpDKikVx1MDAwNPlHIKM1oI7YhFx1MDAwNVlgpmBY0SdswbBcZrxSuD6idG+sLDdASlwiuYYkRoNOJGpCZUTYtTKScFx1MDAwNUhcdTAwMTOg3lx1MDAxZiqNby2BqVx1MDAxNDuVPfLuNCOtlG7rYkjhts5cdTAwMTRDjMaE0umzmIprjs+jy7NQRc2Ts1x1MDAwZebru79cdTAwMGVcbqhlubZ10Vx1MDAxONJlu1kpt9BMVyaG27pcdTAwMThky3eGXHRNKFBNMcM8f1tcdTAwMTeCXHUwMDE4J0VcdTAwMGIrqKCIMUj9qWRcdTAwMDVcdTAwMTO1IeBcdTAwMWFcdTAwMGXukrTEsq/jLs1yXHUwMDE2ur+LgoQwtfXmtPu7vLt/6KrT7Vx1MDAxZUbwyFx1MDAxMSlCW3u1++wneczqjeferk3Y1bhcdTAwMWVcdTAwMWbW5LhcdTAwMTEsQlxc29L//H73+7/nXHUwMDBiXHUwMDAzXCIifQ== MarginPaddingContent areaBorderHeightWidth"},{"location":"guide/styles/#width-and-height","title":"Width and height","text":"

    Setting the width restricts the number of columns used by a widget, and setting the height restricts the number of rows. Let's look at an example which sets both dimensions.

    dimensions01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.height = 10\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    This code produces the following result.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path.

    Note how the text wraps in the widget, and is cropped because it doesn't fit in the space provided.

    "},{"location":"guide/styles/#auto-dimensions","title":"Auto dimensions","text":"

    In practice, we generally want the size of a widget to adapt to its content, which we can do by setting a dimension to \"auto\".

    Let's set the height to auto and see what happens.

    dimensions02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.height = \"auto\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    If you run this you will see the height of the widget now grows to accommodate the full text:

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    "},{"location":"guide/styles/#units","title":"Units","text":"

    Textual offers a few different units which allow you to specify dimensions relative to the screen or container. Relative units can better make use of available space if the user resizes the terminal.

    • Percentage units are given as a number followed by a percent (%) symbol and will set a dimension to a proportion of the widget's parent size. For instance, setting width to \"50%\" will cause a widget to be half the width of its parent.
    • View units are similar to percentage units, but explicitly reference a dimension. The vw unit sets a dimension to a percentage of the terminal width, and vh sets a dimension to a percentage of the terminal height.
    • The w unit sets a dimension to a percentage of the available width (which may be smaller than the terminal size if the widget is within another widget).
    • The h unit sets a dimension to a percentage of the available height.

    The following example demonstrates applying percentage units:

    dimensions03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.height = \"80%\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    With the width set to \"50%\" and the height set to \"80%\", the widget will keep those relative dimensions when resizing the terminal window:

    60 x 2080 x 30120 x 40

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0 total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0 the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0 its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    "},{"location":"guide/styles/#fr-units","title":"FR units","text":"

    Percentage units can be problematic for some relative values. For instance, if we want to divide the screen into thirds, we would have to set a dimension to 33.3333333333% which is awkward. Textual supports fr units which are often better than percentage-based units for these situations.

    When specifying fr units for a given dimension, Textual will divide the available space by the sum of the fr units on that dimension. That space will then be divided amongst the widgets as a proportion of their individual fr values.

    Let's look at an example. We will create two widgets, one with a height of \"2fr\" and one with a height of \"1fr\".

    dimensions04.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.height = \"2fr\"\n        self.widget2.styles.height = \"1fr\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    The total fr units for height is 3. The first widget will have a screen height of two thirds because its height style is set to 2fr. The second widget's height style is 1fr so its screen height will be one third. Here's what that looks like.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    "},{"location":"guide/styles/#maximum-and-minimums","title":"Maximum and minimums","text":"

    The same units may also be used to set limits on a dimension. The following styles set minimum and maximum sizes and can accept any of the values used in width and height.

    • min-width sets a minimum width.
    • max-width sets a maximum width.
    • min-height sets a minimum height.
    • max-height sets a maximum height.
    "},{"location":"guide/styles/#padding","title":"Padding","text":"

    Padding adds space around your content which can aid readability. Setting padding to an integer will add that number additional rows and columns around the content area. The following example sets padding to 2:

    padding01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.padding = 2\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n

    Notice the additional space around the text:

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past, I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0 Only\u00a0I\u00a0will\u00a0remain.

    You can also set padding to a tuple of two integers which will apply padding to the top/bottom and left/right edges. The following example sets padding to (2, 4) which adds two rows to the top and bottom of the widget, and 4 columns to the left and right of the widget.

    padding02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.padding = (2, 4)\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n

    Compare the output of this example to the previous example:

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0 mind-killer. Fear\u00a0is\u00a0the\u00a0 little-death\u00a0that\u00a0 brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0 pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0 past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0 inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0 gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    You can also set padding to a tuple of four values which applies padding to each edge individually. The first value is the padding for the top of the widget, followed by the right of the widget, then bottom, then left.

    "},{"location":"guide/styles/#border","title":"Border","text":"

    The border style draws a border around a widget. To add a border set styles.border to a tuple of two values. The first value is the border type, which should be a string. The second value is the border color which will accept any value that works with color and background.

    The following example adds a border around a widget:

    border01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BorderApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Label(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.border = (\"heavy\", \"yellow\")\n\n\nif __name__ == \"__main__\":\n    app = BorderApp()\n    app.run()\n

    Here is the result:

    BorderApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    There are many other border types. Run the following from the command prompt to preview them.

    textual borders\n
    "},{"location":"guide/styles/#title-alignment","title":"Title alignment","text":"

    Widgets have two attributes, border_title and border_subtitle which (if set) will be displayed within the border. The border_title attribute is displayed in the top border, and border_subtitle is displayed in the bottom border.

    There are two styles to set the alignment of these border labels, which may be set to \"left\", \"right\", or \"center\".

    • border-title-align sets the alignment of the title, which defaults to \"left\".
    • border-subtitle-align sets the alignment of the subtitle, which defaults to \"right\".

    The following example sets both titles and changes the alignment of the title (top) to \"center\".

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BorderTitleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.border = (\"heavy\", \"yellow\")\n        self.widget.border_title = \"Litany Against Fear\"\n        self.widget.border_subtitle = \"by Frank Herbert, in \u201cDune\u201d\"\n        self.widget.styles.border_title_align = \"center\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n

    Note the addition of the titles and their alignments:

    BorderTitleApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Litany\u00a0Against\u00a0Fear\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0by\u00a0Frank\u00a0Herbert,\u00a0in\u00a0\u201cDune\u201d\u00a0\u2501\u251b

    "},{"location":"guide/styles/#outline","title":"Outline","text":"

    Outline is similar to border and is set in the same way. The difference is that outline will not change the size of the widget, and may overlap the content area. The following example sets an outline on a widget:

    outline01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.outline = (\"heavy\", \"yellow\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineApp()\n    app.run()\n

    Notice how the outline overlaps the text in the widget.

    OutlineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503ear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503otal\u00a0obliteration.\u2503 \u2503\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503hrough\u00a0me.\u2503 \u2503nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503he\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Outline can be useful to emphasize a widget, but be mindful that it may obscure your content.

    "},{"location":"guide/styles/#box-sizing","title":"Box sizing","text":"

    When you set padding or border it reduces the size of the widget's content area. In other words, setting padding or border won't change the width or height of the widget.

    This is generally desirable when you arrange things on screen as you can add border or padding without breaking your layout. Occasionally though you may want to keep the size of the content area constant and grow the size of the widget to fit padding and border. The box-sizing style allows you to switch between these two modes.

    If you set box_sizing to \"content-box\" then the space required for padding and border will be added to the widget dimensions. The default value of box_sizing is \"border-box\". Compare the box model diagram for content-box to the box model for border-box.

    content-boxborder-box

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT28pcdTAwMTL9nl9Bcb/Gysz0bJ2qV6/Yl1x1MDAxMCAsXHTJq1spYVx1MDAwYizwXHUwMDE2W8bArfz31yNcdTAwMTZJliWwMY6TukpCsEaWRjN9Tp/uWf55s7CwXHUwMDE43XSCxfdcdTAwMGKLwXXVb4S1rj9YfOvOX1x1MDAwNd1e2G5RkYg/99r9bjW+slx1MDAxZUWd3vt375p+9zKIOlxyv1x1MDAxYXhXYa/vN3pRv1x1MDAxNra9arv5LoyCZu+/7ueu31xm/tNpN2tR10tcdTAwMWVSXHRqYdTu3j0raFx1MDAwNM2gXHUwMDE19eju/6PPXHUwMDBiXHUwMDBi/8Q/U7XrXHUwMDA21chvnTeC+Fx1MDAwYnFRUkGpzPDZ3XYrrqxUXFxLieyxPOyt0tOioEaFZ1TjIClxp1x1MDAxNtVJX+2Y48anXHUwMDBmK8tcdTAwMDfXjWZ40lo+Tlx1MDAxZXpcdTAwMTY2XHUwMDFhh9FN464h/Gq9301VqVx1MDAxN3Xbl8GXsFx1MDAxNtWpnFx1MDAwZp1//F6tXHUwMDFkuVxuPFx1MDAxNnfb/fN6K+j1Ml9qd/xqXHUwMDE43bhzLKn/XSO8X0jOXFzTp1xuXGJcdTAwMGa1NVxcXHUwMDFhRW9rRNIg7lx1MDAwNkJ5Qlx1MDAxYtDKMlx1MDAwYkYrXHUwMDA1Q1VbaTeoL6hqf7H4SOp26lcvz6mCrdrjNVHXb/U6fpd6LLlucP/SiqFcdTAwMDfGXG4trFx1MDAwMouQvE89XGLP65HrXHUwMDEzwT1mXHUwMDE1glF3T9NJbYK4Y1x1MDAwNENtuNaQvKWrQ2erXHUwMDE2m8jfw1xyW/e7nfv2W+y5XHUwMDBmqfq7qq9ccttX2sZSnf/j+uvG9tnJPr+48LdVyGorXHUwMDFmLlx1MDAwZVx1MDAxZe+VMUi/221cdTAwMGZcdTAwMTZcdTAwMWZLft7/llSt36n5d2bmXkRIxplcdTAwMTQqeaFG2Lqkwla/0UjOtauXiWXGZ3++nVx1MDAwMFx1MDAxMFx1MDAxYaFcdTAwMTBcdTAwMTCAXHUwMDFjJTPPR0TQ6e5o4P5t31x1MDAxZixvfP+6Jzu6WYCIXpvQPTZcdTAwMWWGvvVcdTAwMTRcdTAwMWPgKTSA8VxiXHUwMDAymlx1MDAxYq1RSc6MzKCBc/C44Fx1MDAxYYAjWSAyVYhcdTAwMDZ1XHUwMDA2taosRcNfsqqDM5VHXHUwMDAyKONZpSTBUuVBIFx1MDAxNHpcdTAwMDJcdLNMXHUwMDAyMDKMXHUwMDFjXGK4Je5ijIOdLVxiqns7X+snm8e1XHUwMDFixFZ/l3dMvVx1MDAxNv6GIJBWXHUwMDE3gYBcYlxuXGZcIpfPXHUwMDA2XHUwMDAxRqfB7sdGY1x1MDAxYsz+5kr9495BuLkxmVtcdTAwMTCFbsHv1afrXHUwMDE2XHUwMDEwPCEtKs6UloZZkcWBXHUwMDA2jyhcdTAwMThAKaG4JNdQiINAXHUwMDFi81x1MDAxMq+Q7vNHXGJwaYdtXHUwMDFlXHUwMDA01YLqNWOT34K1o5Poy97H9kmwXHUwMDFmNcLLz8fV76NNPlxurqOUxb8tu+3h0fopu8L95tGn6HJ5aXn1eFuL5yGp9L5Tr27m6rfPfeCvw32mnmmlamxcdTAwMTHkpSaq1TiG37tcdTAwMTZcdTAwMWSz/vXHSudQXpyurP6wrZrembJcdTAwMTJcdTAwMWPT8z2NeOdWUFgrNaAwRmJcdTAwMDbxgNZcdTAwMDPpXHUwMDA0IEeUWlxmU9H0ZKBcdTAwMTZ5vFx1MDAwYjVcZndcdTAwMGVcbiW5YvNcbjpvmsaYdHq7XHUwMDE1XHUwMDFkhrdBrFEzZ9f9Zti4yfRbbKVU049+9zxspduyXHUwMDE30DOD2Mdnrl5qhOfOjlx1MDAxN1x1MDAxYsFZ1sCjkFx1MDAwMqfH4qidevMqPd2n23W3asNv0e6G9GS/cZStyUTYXHUwMDAyI1xusSWsZEKq57vTpUF978dutHVzc1xm6+2br6dflr75M4yy2ITgXCLhiNJyi1JcdTAwMDCyrDtcdTAwMDVhXHR65G+F0lrSP/Nq6EpcdNoydGkjKVx1MDAxYzMptz9cdTAwMTNvurRdw9Wr3dMts1x1MDAwNlx1MDAxZjuDXHK5PMDOL1x1MDAxM5Avw+6+X6uFrfN5XHUwMDAw70NVJvOMglx1MDAwZp99QC93XHUwMDAyUY+RXCIpl1x1MDAxZvNcbl5yfSVaWFxi7YlcdTAwMTlpYTlCXHUwMDBii9Tz7tHLhdRcbq36g3wj5PC1QsVUq1x1MDAwNWorfzTIcDTIqvStoFtcdTAwMDKzZlirpVx1MDAwM8Ms0p5cbuiGwZepZylcdTAwMDLLY1IsXHUwMDE0qFxcOM/CpFx1MDAxNM9cdTAwMDZitX/bWPmxXHUwMDE0+T40XHUwMDBlj1pcdTAwMWbPw5NOrVx1MDAwMIjVbrvXq9T9qFovXHUwMDAyoyxcdTAwMDLj1FWqS9BQkIekQJXT5CqDRc6ZR1wilYFVxlpGTqxcdTAwMTCLz8jPlGLx6Vx1MDAxY1xyXHUwMDEyXHUwMDE56Lxv1Vx1MDAxNtEgn3F+ctnfq96eXW037GF7XHUwMDBm+X57U/k4hYCyxj5/qHZPbtesXHUwMDFhyNslXFw7XHK7dj5TPnfPXHUwMDFmXHUwMDA1rZLgT3AgzjdqXGYnV97UY2OrMOkzdWxxMmlkoLlLsFx1MDAxYiVTuVx1MDAxNHdcdTAwMDPJOVx1MDAxNVtcdTAwMGLUXHUwMDFhWnElhlE/PZVcbpw8LpJcdTAwMWIzXHUwMDFjJLd6xFBcdTAwMDBYj0JRbUFw61x1MDAwNLVcdTAwMWPGXHUwMDE5xalWKlx1MDAxNJMgLa7qrL1gL/K70XLYiqXa+1x1MDAxNNjIXHUwMDEzVvtxt3qMSWRKWmpdgVxmXHUwMDEznC2e+524kz1uUFx1MDAxYdTuMomJOFh4XHUwMDFjK7vzYitwc7Mp+ss7XHUwMDFi+uhi/Zj8WCNYekDnI+ZcdTAwMTeDVq20Slx1MDAxNeZRXGKHwFx1MDAxNZBp0N8kdnmslPBcdTAwMTCpOiCQrjNaK1NUqdFuKVepht+LVtrNZuiU3n47bEXDTVx1MDAxY7flklx1MDAwM3w98HP6mF4qXTbMXGZcdTAwMWR3xyyjJr8tJKCJPzz+/vfbkVdcdTAwMTeasjsqOStObvcm/f+4op2DKkxhc1x0zKF7XGZKXHUwMDFibSwzpbTJpLvlnlx1MDAxMZI4nNpfMMwltdBcdTAwMTOCsKKEJTzxYrXw4qRW0lx1MDAxYiVht+JEwZKzXHUwMDE5R90v0Fx1MDAwNq/gw8eJXG7yUfdyu1tLi/tfXHUwMDE3dN/XZDJJwpUsnJjA3aBcdTAwMWaF3qlBqqfwW67SpjNcdTAwMDY1deyCXHUwMDE0XHUwMDFlkMKnuJskPcEziVx1MDAwMe/kXGLzNLNGXHUwMDEzsIFcdTAwMWPecEJgeuAlXHUwMDE3p7R2ro34W1oxXCJBTSxPjKtBMpD0x/JcXFAuXGYyhlx1MDAwMn9TNTLsP59cdTAwMTJcdTAwMDWGoTVGS01q0VKclFx1MDAxM1x1MDAwNdazmjqOSJm5aTYyrWX+dE1QaFDuqORtaUxRUMwqXHUwMDE0y1x1MDAxNFx1MDAwNjqMXHUwMDE0PSGNP19cdTAwMTWUz3mZY1bhhFxyXHUwMDE0SiphWHaCh+SS7NJYRnJNkVx1MDAwNac4dvqsotHB06lngopMJf3TtGKssFx1MDAwNFx1MDAxMUZcdTAwMTdwqfNRXHUwMDBlt4bCXHUwMDAxnCRT/9vxXG7ziClcdTAwMTSBQ3CjiFaS5khcdTAwMDKgXCJcdTAwMWFcdTAwMTk9O+83p5FcIlx1MDAwYnJH3nbGpJFYNo1gXHUwMDExhOJMpDUomLYqueIpXHUwMDEy0cd0Tn7b3T9cdTAwMWaovY3o1lZcdTAwMDbt7mQkMruxcsmUZ610Q+HkwFxmXHUwMDFmykJq4Vx1MDAxMblQkVx1MDAxNKhcZvLXXHUwMDFizpPoWdTU4kZcbklcdTAwMDYgR1xm7zGPWSlcdTAwMDGsJDVlNaRH9Vx1MDAxZcSJdrGRSc0km1x0iVx1MDAxMM+mXHUwMDA2l16DRLI3my62M2VTXHUwMDA1dnGvuqMyokOnXHUwMDA0bVxyavjsI7RcdTAwMTGIbUhrPz/qODw+Xr3+hD++NU5cdTAwMDbtlVUxOPig+nNcdTAwMGZtLjwmjFFcdTAwMWFJXHUwMDA0gM7mQIH0gbGK2oFcdTAwMTSEpk+vlzKQpJBcdTAwMTVX6Vx1MDAxMYRcdTAwMTSkIVx1MDAxNo9mxLxcdTAwMThO6Fx1MDAxMnbWUFx1MDAxNoCQxDz/QvnhyPeiOypJXHUwMDA3Tkvbo8XhsylpT0EygHq+tF/fXGYvwP9yeHKw9v3G9PR688BuzT10XHUwMDAxPDBAYVxmMi7lkFems1x1MDAxZfltyzl55Hio/lx1MDAxNaW9Mlx1MDAwNlx1MDAxMJVcdTAwMDYg8YVyRPaPOF5cdTAwMTC9W+DGpfRVzidzelx1MDAwN6TXkDNW9lxc8pQpTWX8XCIjxPXF8vbpkeZXl37YPFx1MDAxZfRcdTAwMWHLXHUwMDExT09cdTAwMWRNZ1x1MDAxMShcdTAwMDJcdTAwMTbIKFZcdTAwMDOOViPafFx1MDAxZYFTME2UJ8m6XHUwMDE1oHrAU8FcYsZrksirav1KsU3FxTlzmlx1MDAxNq9wLorlvlx1MDAxMoxcdTAwMTGNyedrgsp1/fvR5dbFWuvbh481pj/rw09zPzVWUiylwFquSbNcIiP1lWVcdTAwMTZccp5QXHUwMDAy3ChcdTAwMGXxrFx1MDAxNq+3Ror8XHUwMDA2Z4JcZl4yN8/BwKj5fORzgOojjXGLXHUwMDE1VC5nXHUwMDAwZCDaoJpgqvyLiIXE6lxcXHUwMDEwXHUwMDBi8zhDNMqNgVx0hdqkoPSYndQgUbhoXHUwMDE2uFL4h7JKiTm5Y9iQxuSUosFJa1xus5DWcCZcdTAwMTlcdTAwMWIjXHRZ3uvzSihMeU5cdTAwMTRKaS1YJYaUikBPXHUwMDEzj1x1MDAxM4ujdvmU11MqKlx1MDAwMWTJuCRIt1x1MDAxYc6KXHRiipeMS5b7ioypjTVnqVxc3Jbe98HuS2hutuOdm3e9ljKAXzXeeV+TUkYoyjtcdTAwMTBcdTAwMWZcdTAwMTdSQiwySM2PkXhY2j483Fplllx1MDAxZKxcdTAwMWT8WGnsbKnjrVx0XHUwMDA3JmbHXHTIPa1Rc5JcdTAwMTkk45TKJlx1MDAxZVB6XHUwMDFjhdVCXHUwMDE40NrK11t/wz3rVsBcdTAwMDI9xFxyQOmU/EtcdTAwMTSG8khcdTAwMDVZXHUwMDA0i0ZKrUx+XHQquVxmdGNcdTAwMTMzjl6IU8FMgr4/PFx1MDAwZlEp7ta4ON+jYzr7XCJoW1W4+EdwXHUwMDA2XGJcIj2g/uTKOlx1MDAxZV2cXHUwMDFlX0U7387WQrH+5Wrtw+lg3pFNze2RXHUwMDE3Z8LSoVx1MDAxZIqz0HbJIGFcdTAwMTW5e+Pmmr4mtEFow9yQXHUwMDExcVxijExLMM+6OaBWaYoxOeaCh3heoeapqGI2uDYgXne88bfFdUGfxqW57lx1MDAxY1x1MDAxM9UlXHUwMDEzlFThsiDBlFx1MDAwMW7ZXHUwMDE4XHUwMDEzXGZbS2ebn7+sRlx1MDAxYqe3N7jx+Vx1MDAxY7aD02kvXGaa/nxp48JEQ3yqKebnILJcdTAwMTOUXGZRKvlrZrTlbjFcdTAwMWRcdTAwMGVVbHq4ttTLwjDDrTI8PTybXHUwMDFhMDBWulx1MDAwNFx1MDAxYVx1MDAwMFx1MDAxOIGc59bPc0GBl8suzTgrQO2X0lxi089cbny++bz27Wj95OKK0LpcdTAwMTNt3X5a2d0uSDdyQOGmlFx1MDAxOVCMJNaIqcxuzlx1MDAxOTlHaigmKTTmf2y6scik4sK8NU2NVqB4taFVaMmN4vPXXG53t0Cavf7tdme9YTe+Nb/uhLti3lnFLVx1MDAxNSbbc0pcZrQxmF1taKRHoolcdTAwMTlFP5hg/PXEgqWIg7ulZZLiLyPtiEQj97hcdTAwMTVcdTAwMWNccuNuzbKFXHUwMDE0wz2QilIxoGZMKsJcdTAwMWGchlp4MakwjyCkXGJJWjO00pr0eox7SqFWXHUwMDE0JFx1MDAwMJWhdpTMilx1MDAwN+X9p3FKoUG5o5KzpTEppSjZaIrXTYJbiWdcdTAwMTV//mSl8n6fU0LhXHUwMDE2PWDAOVx1MDAxMDzIv1x1MDAwZi3r0tTyUlPUrIxb7FaypdWLk42JPirbe4BcIiVyrVx1MDAwMFx1MDAxM0RcdTAwMTgvyTaWS9CMrY2VbSx3QqX3fTD8XHUwMDEypptttvHOeFNcdTAwMDbwq5KNd1x1MDAxNZlMY5BwK5RcdTAwMThWU4huxtjdq3yjprmdXHUwMDAzXHKeUlJQjOYmobCko+M5TtJlXCKZXHUwMDExQFElXHRcdTAwMTBePJxJN/Lli4YzyYJJSVx1MDAxMztcdItSKVx1MDAxY7U9ifBQkY7Qd9tUMsjHLmi15NJMsunjbzdcdLriVoFyt5OMdfNL2ajhS+2h5syCllTKpEzJ96yQXHUwMDE5vZdBToD8TjKjUmJUcXnensaUXHUwMDFhxdFLSnRcdTAwMGUxXHUwMDBiUiBFfTVcdTAwMDazlO+FM7fMXCI8a4lcdTAwMTZAo0tGXHLNnpTK01x1MDAxMq1LXHUwMDBmoYSS9ZYvJ1x1MDAxNivjdVx1MDAwMlx1MDAxNH0wRcH6qOjFepriSW7BXHUwMDA1llx1MDAxNvJrtrj7srRcdTAwMTNtJjtcdTAwMGa8Mpo9JDOaXkpcdTAwMTjLjeYmtfjoIVwioV5cIk/odlx1MDAwNFaotMoss8jQx+jNuHL08WfEL5Vio3JH3pzGpJXS7V+wZFx1MDAxNlx1MDAxNrqlMEaPseji5lxmev1PW8dw+vX6y/fT6569Piqa3jlX279I5lHrut0nQIGgTshcdTAwMGWluP15rZvFgi5nJYUuli4v3/+Fe6SUXGZcdTAwMTZtXHUwMDAwIzxcdTAwMDBhXHUwMDE41aFgpjZcdTAwMTlcdTAwMTEhkU8yU/vfnWCet1x1MDAxM8yb+5su+p3OYUS3fCRFauuwdlx1MDAxZv8kt1m8XG6DwfKInZrP4sNVOW5cdTAwMDSHkMC19D8/3/z8P1KdJ/cifQ== MarginPaddingContent areaBorderHeightWidth

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9vr+CYr/G2nn0vFJ161x1MDAxNs9cdTAwMTBcdTAwMTJcYpCsgdzaSim2sFx1MDAwNbJlZFx1MDAxMSBb+e+3RybWw5KwjU2c7CqpXHUwMDA0NLLU1vQ5fbrn8fdva2vr8f3AW3+5tu7dtdzAb0fu7fpcdTAwMGJ7/otcdTAwMTdccv2wj00s+X1cdTAwMTjeRK3kym5cdTAwMWNcdTAwMGaGL//4o+dGV148XGLclud88Yc3bjCMb9p+6LTC3lx1MDAxZn7s9Yb/tf9cdTAwMWW6Pe8/g7DXjiMnfUjDa/txXHUwMDE4jZ7lXHUwMDA1Xs/rx0O8+//w97W1v5N/M9ZFXit2+53ASz6QNKVcdTAwMDaCkMWzh2E/MVZIXHUwMDAyyihcdTAwMTi3+8NtfFrstbHxXHUwMDAyLfbSXHUwMDE2e2r9rue/8+NmcPL57IB5XHUwMDE3LvGvXHUwMDA2nfShXHUwMDE3flx1MDAxMLyP74PRi3Bb3ZsoY9IwjsIr79Rvx11sp4Xz48+1w9hcdTAwMWEwbo7Cm06371xyh7lcdTAwMGaFXHUwMDAzt+XH9/ZcdTAwMWMh47Ojl/ByLT1zh79x7nBKtNRCcVwiOOhxq/082GaiOVx1MDAxN1xmXHUwMDE4SC5cdTAwMGKGbYVcdTAwMDH2XHUwMDA0XHUwMDFh9jtJjtSyz27rqoPm9dvja+LI7Vx1MDAwZlx1MDAwN26E/ZVed/vwlVx1MDAwNTFcdTAwMGVXmkmmXHUwMDA114an36br+Z1ubI1h1CFaXHUwMDE4rsToaVx1MDAxOWu8pFskp5wrotm4wZoweN1O/OOv4lvtutHg4eWtXHUwMDBm7S9cdTAwMTnzreU7RefKOlim51xy8TZO7k7J3edBXHUwMDEzneY0eHfahPG9ct7oRlF4uz5u+fbwU2razaDtjnyMSslcdTAwMTlcdTAwMTB8+6D5uD3w+1fY2L9cdIL0XFzYukrdMjn77cVcdTAwMWNokIZVoYFqYjRcdTAwMTDK2NR4ODm97MZcdTAwMWLhtup97Hxccj5+eCvfvb2vwMMwRGzPjIbCp1x1MDAxZVx1MDAwM1x1MDAwM39cZlx1MDAwYpSgt1x1MDAwYsZcYjFcdTAwMDZ9jGb8yH5eoP9RySRBJzXEyKJdKVx1MDAxOMRcdTAwMDVvt6BcdTAwMTZcZr9DS3pcdTAwMTdiXHUwMDEyXGJcXChHXHUwMDBiXHUwMDAxUisxiVx1MDAwMSaMw4yRmiA0XHRcdTAwMTOTXHUwMDE44JxqLkHA82Kg9e7tefds78/2vTH9m0M6UN22/1x1MDAxM2JcdTAwMDA0VGKAUMJcdTAwMDDUXGYxYftcXJxcXDc9ev9n8+xEXHUwMDBm995cdTAwMDSXXHUwMDFmTueLXHSsXG5cdTAwMDVtd9hdbEyg6GTC8jBwXHLAqVB5XHUwMDFjKOooQzlcdTAwMDEjXGZjylTiwJNKPSUooH9PQoBmYtSDzzNcboDxKXv5szj94U5M3vpcdTAwMWRcdTAwMTF/jravTuLgXCLce/Wm3Olj7y7O+PyLutteXGZOXulX/a2d/YbYi09aXHUwMDAxOT6/m1x1MDAwZUtcdTAwMTX3zVnxYtov8uMgmrMzKyiVrkInSCRFaVx1MDAxNJlcdTAwMWGcXHUwMDA3XHUwMDFmVaB29oP30cX713s3w43b1n60YME2Y4iaQq8x6TCigVx1MDAxOcWI4kJCXHUwMDBlm/hcblx1MDAxY661tGKNXHUwMDBiMLxcdTAwMTKbT1x1MDAxNWySTUKTiVwiMlx1MDAwMcMkXG6yZUSjRTpj2ulhP37vf7XvnZHc2V2351x1MDAwN/e5fku81DqSXHUwMDFidfx+9lVcdTAwMGU9fOZIN+Wu3lxi/I714/XAu8g7eOxjfjNujsPMN2/h0128XfS6XfxcdTAwMTZh5OOT3eBD3pK5sMVcdTAwMTWtwlx1MDAxNkXppylcdTAwMTAxfeR7c35w+nZn+PntznHcUTuvOifdT3NGvrmyITJcdTAwMWa6qHFAXHUwMDAxXHUwMDEwXGZrhGNOkkeX4tIxkoNAiYgvg6uloSujMWrQRVxyXHUwMDEzXHUwMDEyreR68fCqXHUwMDBiUFFHXHUwMDFlNFx1MDAwM/r5LrzqkyDUp2Hv4vCHib2ngffIbbf9fmdcdTAwMTXQ+92U+UJj5itcdTAwMTfhK4iNXHUwMDE3POMmj8G3XtmsKnyphDrhisjF5OlZhCuUXGJXllx0xlx1MDAwZvhcdTAwMDVcblx1MDAxOMzR8X+d8MgnXHUwMDEwtoXNaNVcdTAwMWG+K7dcdTAwMWNmplx1MDAxY2Yt/JRcdTAwMTfVXHUwMDAwree329k0Lo+1x7KvXCL8cnbWYrA+gzSqXHUwMDEyiEQxRtksXHUwMDE5ZGfbb59s3HXce/ewXHUwMDE37t1cZm72gFRcdTAwMDCxXHUwMDE1hcNho+vGrW5cdTAwMTVcdTAwMTihXG6MXHUwMDBiXHUwMDE3qkkxXHUwMDA1OCgjXHUwMDA0l0LlXHUwMDBii4JqR6AyJJpQplxyiEooTlFLqYXi4/VcdTAwMTSDykaWXHUwMDA0V8JcdTAwMTlVmstnriY2L11/6+p6p8ng3WB/+PngVu2cPyn7XHUwMDFi3fe8/fVi19/a273YdU9PT10uP7Y+rWaFZvT8MmwhdqrAhXxcdTAwMGWU80xF7DFs1b/pmbFVWaFZOLZcdTAwMThcdTAwMDOHalx1MDAxNKuGIbKooDxcdTAwMDcujSrWgNFcdTAwMDQ1LJGELS9cdOTUYWAoXHUwMDAzRTlQLUuq9txcdTAwMDLdSI1g0txcdTAwMTBcdTAwMDZFnKH1wsBcXCliYulzx8Bh7Ebxpt9PpNrLXGbSMFx1MDAwZbZukk51XGJKXGZMXHUwMDEwNL5cXFsqTkG23nFcdTAwMDdJXHUwMDE3O1x1MDAxNN1VXHUwMDE5aS9cdTAwMDOTdtHaeExrXHUwMDE0w+KtY3bNdjY25T5rvTmmh/y8t/1cdTAwMWSaY8Cve/12rUlccuIwXHJcdTAwMDYlXHUwMDExJ1rjX6kmjGKOMWhcdTAwMGVnXHUwMDA2r1NSXG5VZVR5UJowKnCH8VbY6/lW51x1MDAxZIV+Py6+4uRdbli0dz13Qlx1MDAxZuOXyrZcdTAwMTVpYWDvmKfT9Ke1XHUwMDE0Mskv45//elF6daUn26Mx4cTp7X7L/j+raEfXXHUwMDE3xdNjsYCRx8pYPT2hlTvLs1x1MDAxMtqcwlx1MDAxZDRihitqhLKygFx1MDAxNataysGEXFxK7Fx1MDAxZMy6dbVaeHJVK33ZdXk31cJIhO3PI1xyllx1MDAxMMJnyVxuJvPuzTBqZ8X9j0u7XHUwMDFmLJlPkVBUlpVcYmbSKIXeP/2gab1IW8yA0cLRXHUwMDBiRDtSXHUwMDEwJE4g3GiTwedIjmAzZkVcdTAwMWP9TGnk0KWhXHUwMDE3g5yQ0lx1MDAwNjdkcNCspESNPM/xXHUwMDFhjpaCLVx1MDAxM9CJpNygnVx1MDAxNNRcdTAwMWMltVWQI8VcdTAwMDD6mCpQxGilJGDXXGKt02g0Vlx1MDAwNdrRkmEqRFxmkahUICtmfnVRUOlP9mhMutKMqqCaVLjkxdNjUsHwx7iZZWZS/fyUXHUwMDE1Jlx1MDAxNVx1MDAwMIZZu1accZpGkIRTXHUwMDE4qlWCnlx1MDAwYlRcdTAwMTCioLqU93ROkYZcdTAwMTJI1LN9Xiqcc6SiNNOIXHUwMDEwNFx1MDAwNclDTiQ50tY5XHUwMDA05mT/XHUwMDAwViHYb0YgNFx1MDAxOFVcdTAwMDJJJX1cdTAwMWJp/lNFXCLlk+h+clx1MDAxMqlyIHtMus6MJJJoplx1MDAxMlx1MDAwZdGiejyAII1cdTAwMGL0yOnHynfc+91B0CHNe9o6+nIs2NZ1pz9cdTAwMWaFPN9YOShw8ItcIj2D4sJQmtclglJcdTAwMDdTPWCUXCLhSrk8XVwiMN2XmOyXliCpU6xOfs8yXHUwMDEwPVxcSzPPdMbVJo38zVx1MDAxNovlXFzbQoFcXNKL9lx1MDAxOPffgoCLwKxcdTAwMDIuYlpcYok6e2rcfo1JTM8uL4/5K7155DF+pE/C1cetclx1MDAwNFKmRKFjU6g8bJUmXHUwMDBlalx1MDAwMi2JoFx1MDAxY4XZ8opcdTAwMDGA2ldQkVx1MDAxZFx1MDAxYlx1MDAxOKOWODyRhSWwTSZcdTAwMGZgXHUwMDE2NMeg3r+wTdpcdTAwMTZcbtvJXrRHI+3ARal2wyonp1FJmJI0XHUwMDFik1x1MDAxZkNu97T1seG7uumfiNthXHUwMDE0XGbgar9q4G91kGvszFHCXGZTzNhZNPmBXHTJXHUwMDE0akOpNGWYWGUnky9etVx1MDAxYrt4g9tcdTAwMDVcdTAwMDE2PYCSup5cdTAwMWShxORBg5DKlutFxpxcdTAwMTGUNbdF2J82XHUwMDAwl2f8XHUwMDA0iOBcXFx1MDAxYqFcdTAwMTRXoFVWi49HJ1BcdTAwMTNJ4Fx1MDAxOG64ydVcdTAwMDRyYl1cXEf87O71wac+XHUwMDA0fPPr8aG5fffpkbGJZXLIUmV8o9qlkuZJb1pcdTAwMTSvUKKqS4xIYVxcIK9MX2I87X9yNztcdTAwMWJcdTAwMDfXsblcdTAwMGVft44/6aG6Wn1iUY62Yy+Gg9CSZtZtJcSiqMOMXaYkUVx1MDAxNDC+vFx1MDAxMU9MXHUwMDE5gFx1MDAxM84lXHUwMDEwUJiz8ZJygKHoKlxuXHUwMDEzXHUwMDBlwTRXYrJcdTAwMTiAmFI8y0k/O62gMqaYvFwiWVwiLlxieiOdZFx1MDAxNe1IXHUwMDBlXHUwMDE4XHUwMDE4XGYjnFxuUVlH/EexSrU72aPoSDMyStWoo1bV1UXQXHUwMDA0I7NR009cdTAwMTUs769V51x1MDAxMyRcZoxvmml0VU15fqWX5MqRQlx1MDAwM1x1MDAwM8XxXmp583xTXHUwMDE41I43XHUwMDAyp9RcdTAwMGXaz05cdTAwMThPXHUwMDE5bqyPXHUwMDE0OVebaSZSvbStve93v19cdTAwMDbNzTWMuTfqtYxcdTAwMDP8qGHMXHUwMDA3S2pcdTAwMTmhquaQXHUwMDFioyzOq1wiRlx1MDAxYqpcdTAwMTlMP1x1MDAwZmEn2N30olx1MDAwZsHB9kWjcfl+b/NAdVc+d7GpXHUwMDBiQt1cYm2IMpLnU1x1MDAxN0GoYydvUmaFs1x1MDAxMMurOlx1MDAxMEdcdTAwMTLDkPIxg0LuZ7REYYBwXGJBMapcdTAwMDU2M010SfHQ2HnFdJ6lXHUwMDAxqyAyfrUqRHWv2qMx2aEzxvoqZGeHxlxuwFaCSsblXGZcdTAwMDOJW/os+qT3z/eGTdlvXHUwMDFmeTJqbp6tOq5BMocwzKE0SClcdTAwMDXPZ1x1MDAwZWCsLtVSXHUwMDAyaFBMXHUwMDE2J0gvXHUwMDEy11x1MDAxYWWVlkJJSqTWZSVcdLA7XHUwMDFjoFxmVIKJZMnVXHUwMDA0rDm26rmUwFNRndryL6rHXHUwMDE3VPapPVx1MDAxYVx1MDAxM905I6irS1x1MDAwMjWLXGYwvbKlRlx1MDAwZdOXXHUwMDA0ro/8+y3SNedcdTAwMWa67Y7vn53e7u6t/ChcdTAwMDFcdTAwMDNwXGaVRktjXGLDfLqwdYnmXHUwMDBlxUxTKWqzTbnEcVx1MDAwMlx1MDAwNKVDsEeYpkTR7Gq8zCCf5lx1MDAwMrBdc66YoXRiXHI7xYTDUFwi5plTuFxu0C4vNqKjUtSPJNlMXHUwMDA3aHZcdTAwMDZNWmy0U42JXHUwMDExXHUwMDA0OFx1MDAxM7Sy2rjt9ptnPGTn9+JcdTAwMGJcdTAwMWKqo2PYV41ftS7QqPSpUeuEOy2MWZioLFx1MDAwZVx1MDAxOCWTXYCmXHUwMDE3XGaXYuvd61x1MDAxYlxugygkV1+uXHUwMDA2zd1ccrq/6sRiXHUwMDE3XHUwMDAxK1wiXHUwMDExiShcdTAwMDeULTlcdTAwMTaIhTmMMrthXHUwMDEyXHUwMDAzu6JrebzC7OQ7u9hYKjvJSZcsXG62w1hcdTAwMTQoXHUwMDA2ICGl0lxcZ1x1MDAwMsN3ZtHG2PDzI1KBJTFcdTAwMGJxiCDSXHUwMDBls1x1MDAwMoKEI7WUzDGiXHUwMDBlZVxcM0zXXGJcdTAwMDVM6SqXM/yjmKXaqezRKPGnXHUwMDE5qaWq6qhqJFx1MDAwYrpcdTAwMGJcdTAwMTUzTEcq77FcdTAwMTXnXHUwMDE1JoQjNJN25Vx1MDAwMFxiY1xuq7ZAXHUwMDFhh2qFnopdw1x1MDAxOC9cdTAwMWG2wKJj+uC6oqO09Icq65lcdTAwMTc51IvRnKvNVHWsj0W19/3u98sgu7mqjiPnzTjAjyo6jlxmmU9qQMbFi8NcdTAwMTBUXHUwMDE4JG0mp6851m+atLKTnFxyKjmuXHTlRmCQyqcwXG6sXHUwMDEwwVx1MDAxNIeB3dOnZoJcIlx1MDAwN+7Ck2pcdTAwMTNcdTAwMDSNtSNRo9XYwpTtP8JcdTAwMWMjXHUwMDA0hlx1MDAwNTtcdTAwMDVcdTAwMGWN5ZM5XGbmWUJcdTAwMTIxzz5cXKugNGZbPGGXeVKN6krb7TUwnGaS/1x1MDAwN1xyYreLsVwiXVx1MDAwMrZcdTAwMTKAjIovSJDSrVxuJiTIzyQ0XHUwMDFhNU6VtE/604xKozqJ0ZVFT8G5xD9meq1Rv9nNXG5cdTAwMTNcdTAwMGKVnNi1lPhfcVxupeCONFx1MDAwNLVcYv4k6qZQPp1YNCQrXHUwMDAxJNGWXHUwMDE3oIRX7PIwRmzJSlx1MDAxOM3snKtcIq/gWck5Uz9gifiySiPUQaEnQWNcdTAwMTKjqZJUlVRGhGPXXHUwMDFlUVBcYlx1MDAxZCFFblx1MDAxZEWOPco325pgj18jgWlU+5Q9Jr1pRlap3dzFsMokxqC254ZPTyyt4y+dm6NBcDxodT9s0r1cdTAwMGZfL5tVeyyt2NYu0tFcbr8tui9nupDH2G1yXHI6LbW+bZipnjzx9K1dqMOBVi2sYII5ljUsXHUwMDAzlk/VtjsoamaL88+b4vy8W7zMQoe/fb9xctN1dzB4XHUwMDFm4y3HhIjv2m8/pD7pbda/+N7tZsmGyVx1MDAxN8lhTU5egsWHZ9/0399++/Z/3Vx1MDAxNVx1MDAwZVx1MDAwMiJ9 MarginPaddingContent areaBorderHeightWidth

    The following example creates two widgets with a width of 30, a height of 6, and a border and padding of 1. The first widget has the default box_sizing (\"border-box\"). The second widget sets box_sizing to \"content-box\".

    box_sizing01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BoxSizing(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.width = 30\n        self.widget2.styles.width = 30\n        self.widget1.styles.height = 6\n        self.widget2.styles.height = 6\n        self.widget1.styles.border = (\"heavy\", \"white\")\n        self.widget2.styles.border = (\"heavy\", \"white\")\n        self.widget1.styles.padding = 1\n        self.widget2.styles.padding = 1\n        self.widget2.styles.box_sizing = \"content-box\"\n\n\nif __name__ == \"__main__\":\n    app = BoxSizing()\n    app.run()\n

    The padding and border of the first widget is subtracted from the height leaving only 2 lines in the content area. The second widget also has a height of 6, but the padding and border adds additional height so that the content area remains 6 lines.

    BoxSizing \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u2503 \u2503brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    "},{"location":"guide/styles/#margin","title":"Margin","text":"

    Margin is similar to padding in that it adds space, but unlike padding, margin is outside of the widget's border. It is used to add space between widgets.

    The following example creates two widgets, each with a margin of 2.

    margin01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass MarginApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.border = (\"heavy\", \"white\")\n        self.widget2.styles.border = (\"heavy\", \"white\")\n        self.widget1.styles.margin = 2\n        self.widget2.styles.margin = 2\n\n\nif __name__ == \"__main__\":\n    app = MarginApp()\n    app.run()\n

    Notice how each widget has an additional two rows and columns around the border.

    MarginApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Note

    In the above example both widgets have a margin of 2, but there are only 2 lines of space between the widgets. This is because margins of consecutive widgets overlap. In other words when there are two widgets next to each other Textual picks the greater of the two margins.

    "},{"location":"guide/styles/#more-styles","title":"More styles","text":"

    We've covered the most fundamental styles used by Textual apps, but there are many more which you can use to customize many aspects of how your app looks. See the Styles reference for a comprehensive list.

    In the next chapter we will discuss Textual CSS which is a powerful way of applying styles to widgets that keeps your code free of style attributes.

    "},{"location":"guide/testing/","title":"Testing","text":"

    Code testing is an important part of software development. This chapter will cover how to write tests for your Textual apps.

    "},{"location":"guide/testing/#what-is-testing","title":"What is testing?","text":"

    It is common to write tests alongside your app. A test is simply a function that confirms your app is working correctly.

    Learn more about testing

    We recommend Python Testing with pytest for a comprehensive guide to writing tests.

    "},{"location":"guide/testing/#do-you-need-to-write-tests","title":"Do you need to write tests?","text":"

    The short answer is \"no\", you don't need to write tests.

    In practice however, it is almost always a good idea to write tests. Writing code that is completely bug free is virtually impossible, even for experienced developers. If you want to have confidence that your application will run as you intended it to, then you should write tests. Your test code will help you find bugs early, and alert you if you accidentally break something in the future.

    "},{"location":"guide/testing/#testing-frameworks-for-textual","title":"Testing frameworks for Textual","text":"

    Textual doesn't require any particular test framework. You can use any test framework you are familiar with, but we will be using pytest in this chapter.

    "},{"location":"guide/testing/#testing-apps","title":"Testing apps","text":"

    You can often test Textual code in the same way as any other app, and use similar techniques. But when testing user interface interactions, you may need to use Textual's dedicated test features.

    Let's write a simple Textual app so we can demonstrate how to test it. The following app shows three buttons labelled \"red\", \"green\", and \"blue\". Clicking one of those buttons or pressing a corresponding R, G, and B key will change the background color.

    rgb.pyOutput
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Footer\n\n\nclass RGBApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Horizontal {\n        width: auto;\n        height: auto;\n    }\n    \"\"\"\n\n    BINDINGS = [\n        (\"r\", \"switch_color('red')\", \"Go Red\"),\n        (\"g\", \"switch_color('green')\", \"Go Green\"),\n        (\"b\", \"switch_color('blue')\", \"Go Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Button(\"Red\", id=\"red\")\n            yield Button(\"Green\", id=\"green\")\n            yield Button(\"Blue\", id=\"blue\")\n        yield Footer()\n\n    @on(Button.Pressed)\n    def pressed_button(self, event: Button.Pressed) -> None:\n        assert event.button.id is not None\n        self.action_switch_color(event.button.id)\n\n    def action_switch_color(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = RGBApp()\n    app.run()\n

    RGBApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 RedGreenBlue \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0R\u00a0\u00a0Go\u00a0Red\u00a0\u00a0G\u00a0\u00a0Go\u00a0Green\u00a0\u00a0B\u00a0\u00a0Go\u00a0Blue\u00a0

    Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code. Tests allow us to automate such testing so we can quickly simulate user interactions and check the result.

    To test our simple app we will use the run_test() method on the App class. This replaces the usual call to run() and will run the app in headless mode, which prevents Textual from updating the terminal but otherwise behaves as normal.

    The run_test() method is an async context manager which returns a Pilot object. You can use this object to interact with the app as if you were operating it with a keyboard and mouse.

    Let's look at the tests for the example above:

    test_rgb.py
    from rgb import RGBApp\n\nfrom textual.color import Color\n\n\nasync def test_keys():  # (1)!\n    \"\"\"Test pressing keys has the desired result.\"\"\"\n    app = RGBApp()\n    async with app.run_test() as pilot:  # (2)!\n        # Test pressing the R key\n        await pilot.press(\"r\")  # (3)!\n        assert app.screen.styles.background == Color.parse(\"red\")  # (4)!\n\n        # Test pressing the G key\n        await pilot.press(\"g\")\n        assert app.screen.styles.background == Color.parse(\"green\")\n\n        # Test pressing the B key\n        await pilot.press(\"b\")\n        assert app.screen.styles.background == Color.parse(\"blue\")\n\n        # Test pressing the X key\n        await pilot.press(\"x\")\n        # No binding (so no change to the color)\n        assert app.screen.styles.background == Color.parse(\"blue\")\n\n\nasync def test_buttons():\n    \"\"\"Test pressing keys has the desired result.\"\"\"\n    app = RGBApp()\n    async with app.run_test() as pilot:\n        # Test clicking the \"red\" button\n        await pilot.click(\"#red\")  # (5)!\n        assert app.screen.styles.background == Color.parse(\"red\")\n\n        # Test clicking the \"green\" button\n        await pilot.click(\"#green\")\n        assert app.screen.styles.background == Color.parse(\"green\")\n\n        # Test clicking the \"blue\" button\n        await pilot.click(\"#blue\")\n        assert app.screen.styles.background == Color.parse(\"blue\")\n
    1. The run_test() method requires that it run in a coroutine, so tests must use the async keyword.
    2. This runs the app and returns a Pilot instance we can use to interact with it.
    3. Simulates pressing the R key.
    4. This checks that pressing the R key has resulted in the background color changing.
    5. Simulates clicking on the widget with an id of red (the button labelled \"Red\").

    There are two tests defined in test_rgb.py. The first to test keys and the second to test button clicks. Both tests first construct an instance of the app and then call run_test() to get a Pilot object. The test_keys function simulates key presses with Pilot.press, and test_buttons simulates button clicks with Pilot.click.

    After simulating a user interaction, Textual tests will typically check the state has been updated with an assert statement. The pytest module will record any failures of these assert statements as a test fail.

    If you run the tests with pytest test_rgb.py you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color.

    If you later update this app, and accidentally break this functionality, one or more of your tests will fail. Knowing which test has failed will help you quickly track down where your code was broken.

    "},{"location":"guide/testing/#simulating-key-presses","title":"Simulating key presses","text":"

    We've seen how the press method simulates keys. You can also supply multiple keys to simulate the user typing in to the app. Here's an example of simulating the user typing the word \"hello\".

    await pilot.press(\"h\", \"e\", \"l\", \"l\", \"o\")\n

    Each string creates a single keypress. You can also use the name for non-printable keys (such as \"enter\") and the \"ctrl+\" modifier. These are the same identifiers as used for key events, which you can experiment with by running textual keys.

    "},{"location":"guide/testing/#simulating-clicks","title":"Simulating clicks","text":"

    You can simulate mouse clicks in a similar way with Pilot.click. If you supply a CSS selector Textual will simulate clicking on the matching widget.

    Note

    If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector. This is generally what you want, because a real user would experience the same thing.

    "},{"location":"guide/testing/#clicking-the-screen","title":"Clicking the screen","text":"

    If you don't supply a CSS selector, then the click will be relative to the screen. For example, the following simulates a click at (0, 0):

    await pilot.click()\n
    "},{"location":"guide/testing/#click-offsets","title":"Click offsets","text":"

    If you supply an offset value, it will be added to the coordinates of the simulated click. For example the following line would simulate a click at the coordinates (10, 5).

    await pilot.click(offset=(10, 5))\n

    If you combine this with a selector, then the offset will be relative to the widget. Here's how you would click the line above a button.

    await pilot.click(Button, offset=(0, -1))\n
    "},{"location":"guide/testing/#modifier-keys","title":"Modifier keys","text":"

    You can simulate clicks in combination with modifier keys, by setting the shift, meta, or control parameters. Here's how you could simulate ctrl-clicking a widget with an ID of \"slider\":

    await pilot.click(\"#slider\", control=True)\n
    "},{"location":"guide/testing/#changing-the-screen-size","title":"Changing the screen size","text":"

    The default size of a simulated app is (80, 24). You may want to test what happens when the app has a different size. To do this, set the size parameter of run_test to a different size. For example, here is how you would simulate a terminal resized to 100 columns and 50 lines:

    async with app.run_test(size=(100, 50)) as pilot:\n    ...\n
    "},{"location":"guide/testing/#pausing-the-pilot","title":"Pausing the pilot","text":"

    Some actions in a Textual app won't change the state immediately. For instance, messages may take a moment to bubble from the widget that sent them. If you were to post a message and immediately assert you may find that it fails because the message hasn't yet been processed.

    You can generally solve this by calling pause() which will wait for all pending messages to be processed. You can also supply a delay parameter, which will insert a delay prior to waiting for pending messages.

    "},{"location":"guide/testing/#textuals-tests","title":"Textual's tests","text":"

    Textual itself has a large battery of tests. If you are interested in how we write tests, see the tests/ directory in the Textual repository.

    "},{"location":"guide/testing/#snapshot-testing","title":"Snapshot testing","text":"

    Snapshot testing is the process of recording the output of a test, and comparing it against the output from previous runs.

    Textual uses snapshot testing internally to ensure that the builtin widgets look and function correctly in every release. We've made the pytest plugin we built available for public use.

    The official Textual pytest plugin can help you catch otherwise difficult to detect visual changes in your app.

    It works by generating an SVG screenshot (such as the images in these docs) from your app. If the screenshot changes in any test run, you will have the opportunity to visually compare the new output against previous runs.

    "},{"location":"guide/testing/#installing-the-plugin","title":"Installing the plugin","text":"

    You can install pytest-textual-snapshot using your favorite package manager (pip, poetry, etc.).

    pip install pytest-textual-snapshot\n
    "},{"location":"guide/testing/#creating-a-snapshot-test","title":"Creating a snapshot test","text":"

    With the package installed, you now have access to the snap_compare pytest fixture.

    Let's look at an example of how we'd create a snapshot test for the calculator app below.

    CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 C+/-%\u00f7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 789\u00d7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 456- \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 123+ \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 0.= \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    First, we need to create a new test and specify the path to the Python file containing the app. This path should be relative to the location of the test.

    def test_calculator(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\")\n

    Let's run the test as normal using pytest.

    pytest\n

    When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail. Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to.

    If you open the snapshot report in your browser, you'll see something like this:

    Tip

    You can usually open the link directly from the terminal, but some terminal emulators may require you to hold Ctrl or Cmd while clicking for links to work.

    The report explains that there's \"No history for this test\". It's our job to validate that the initial snapshot looks correct before proceeding. Our calculator is rendering as we expect, so we'll save this snapshot:

    pytest --snapshot-update\n

    Warning

    Only ever run pytest with --snapshot-update if you're happy with how the output looks on the left hand side of the snapshot report. When using --snapshot-update, you're saying \"I'm happy with all of the screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared against\". As such, you should only run pytest --snapshot-update after running pytest and confirming the output looks good.

    Now that our snapshot is saved, if we run pytest (with no arguments) again, the test will pass. This is because the screenshot taken during this test run matches the one we saved earlier.

    "},{"location":"guide/testing/#catching-a-bug","title":"Catching a bug","text":"

    The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed.

    Imagine a new developer joins your team, and tries to make a few changes to the calculator. While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app. When they run pytest, they're presented with a report which reveals the damage:

    On the right, we can see our \"historical\" snapshot - this is the one we saved earlier. On the left is how our app is currently rendering - clearly not how we intended!

    We can click the \"Show difference\" toggle at the top right of the diff to overlay the two versions:

    This reveals another problem, which could easily be missed in a quick visual inspection - our new developer has also deleted the number 4!

    Tip

    Snapshot tests work well in CI on all supported operating systems, and the snapshot report is just an HTML file which can be exported as a build artifact.

    "},{"location":"guide/testing/#pressing-keys","title":"Pressing keys","text":"

    You can simulate pressing keys before the snapshot is captured using the press parameter.

    def test_calculator_pressing_numbers(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\", press=[\"1\", \"2\", \"3\"])\n
    "},{"location":"guide/testing/#changing-the-terminal-size","title":"Changing the terminal size","text":"

    To capture the snapshot with a different terminal size, pass a tuple (width, height) as the terminal_size parameter.

    def test_calculator(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\", terminal_size=(50, 100))\n
    "},{"location":"guide/testing/#running-setup-code","title":"Running setup code","text":"

    You can also run arbitrary code before the snapshot is captured using the run_before parameter.

    In this example, we use run_before to hover the mouse cursor over the widget with ID number-5 before taking the snapshot.

    def test_calculator_hover_number(snap_compare):\n    async def run_before(pilot) -> None:\n        await pilot.hover(\"#number-5\")\n\n    assert snap_compare(\"path/to/calculator.py\", run_before=run_before)\n

    For more information, visit the pytest-textual-snapshot repo on GitHub.

    "},{"location":"guide/widgets/","title":"Widgets","text":"

    In this chapter we will explore widgets in more detail, and how you can create custom widgets of your own.

    "},{"location":"guide/widgets/#what-is-a-widget","title":"What is a widget?","text":"

    A widget is a component of your UI responsible for managing a rectangular region of the screen. Widgets may respond to events in much the same way as an app. In many respects, widgets are like mini-apps.

    Information

    Every widget runs in its own asyncio task.

    "},{"location":"guide/widgets/#custom-widgets","title":"Custom widgets","text":"

    There is a growing collection of builtin widgets in Textual, but you can build entirely custom widgets that work in the same way.

    The first step in building a widget is to import and extend a widget class. This can either be Widget which is the base class of all widgets, or one of its subclasses.

    Let's create a simple custom widget to display a greeting.

    hello01.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\n\n\nclass Hello(Widget):\n    \"\"\"Display a greeting.\"\"\"\n\n    def render(self) -> RenderResult:\n        return \"Hello, [b]World[/b]!\"\n\n\nclass CustomApp(App):\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n

    The highlighted lines define a custom widget class with just a render() method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.

    Note that the text contains tags in square brackets, i.e. [b]. This is console markup which allows you to embed various styles within your content. If you run this you will find that World is in bold.

    CustomApp Hello,\u00a0World!

    This (very simple) custom widget may be styled in the same way as builtin widgets, and targeted with CSS. Let's add some CSS to this app.

    hello02.pyhello02.tcss hello02.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\n\n\nclass Hello(Widget):\n    \"\"\"Display a greeting.\"\"\"\n\n    def render(self) -> RenderResult:\n        return \"Hello, [b]World[/b]!\"\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello02.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    color: $text;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    The addition of the CSS has completely transformed our custom widget.

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHello,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/widgets/#static-widget","title":"Static widget","text":"

    While you can extend the Widget class, a subclass will typically be a better starting point. The Static class is a widget subclass which caches the result of render, and provides an update() method to update the content area.

    Let's use Static to create a widget which cycles through \"hello\" in various languages.

    hello03.pyhello03.tcssOutput hello03.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    def on_mount(self) -> None:\n        self.next_word()\n\n    def on_click(self) -> None:\n        self.next_word()\n\n    def next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"{hello}, [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello03.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello03.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note that there is no render() method on this widget. The Static class is handling the render for us. Instead we call update() when we want to update the content within the widget.

    The next_word method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget.

    "},{"location":"guide/widgets/#default-css","title":"Default CSS","text":"

    When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a DEFAULT_CSS class variable inside your widget class.

    Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code.

    Here's the Hello example again, this time the widget has embedded default CSS:

    hello04.pyhello04.tcssOutput hello04.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Hello {\n        width: 40;\n        height: 9;\n        padding: 1 2;\n        background: $panel;\n        border: $secondary tall;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    def on_mount(self) -> None:\n        self.next_word()\n\n    def on_click(self) -> None:\n        self.next_word()\n\n    def next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"{hello}, [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello04.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello04.tcss
    Screen {\n    align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/widgets/#scoped-css","title":"Scoped CSS","text":"

    Default CSS is scoped by default. All this means is that CSS defined in DEFAULT_CSS will affect the widget and potentially its children only. This is to prevent you from inadvertently breaking an unrelated widget.

    You can disabled scoped CSS by setting the class var SCOPED_CSS to False.

    "},{"location":"guide/widgets/#default-specificity","title":"Default specificity","text":"

    CSS defined within DEFAULT_CSS has an automatically lower specificity than CSS read from either the App's CSS class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.

    "},{"location":"guide/widgets/#text-links","title":"Text links","text":"

    Text in a widget may be marked up with links which perform an action when clicked. Links in console markup use the following format:

    \"Click [@click='app.bell']Me[/]\"\n

    The @click tag introduces a click handler, which runs the app.bell action.

    Let's use markup links in the hello example so that the greeting becomes a link which updates the widget.

    hello05.pyhello05.tcssOutput hello05.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    def on_mount(self) -> None:\n        self.action_next_word()\n\n    def action_next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello05.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello05.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the next_word action which updates the next word.

    "},{"location":"guide/widgets/#border-titles","title":"Border titles","text":"

    Every widget has a border_title and border_subtitle attribute. Setting border_title will display text within the top border, and setting border_subtitle will display text within the bottom border.

    Note

    Border titles will only display if the widget has a border enabled.

    The default value for these attributes is empty string, which disables the title. You can change the default value for the title attributes with the BORDER_TITLE and BORDER_SUBTITLE class variables.

    Let's demonstrate setting a title, both as a class variable and a instance variable:

    hello06.pyhello06.tcssOutput hello06.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    BORDER_TITLE = \"Hello Widget\"  # (1)!\n\n    def on_mount(self) -> None:\n        self.action_next_word()\n        self.border_subtitle = \"Click for next hello\"  # (2)!\n\n    def action_next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello05.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    1. Setting the default for the title attribute via class variable.
    2. Setting subtitle via an instance attribute.
    hello06.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u00a0Hello\u00a0Widget\u00a0\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Click\u00a0for\u00a0next\u00a0hello\u00a0\u2581\u258e

    Note that titles are limited to a single line of text. If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added).

    There are a number of styles that influence how titles are displayed (color and alignment). See the style reference for details.

    "},{"location":"guide/widgets/#rich-renderables","title":"Rich renderables","text":"

    In previous examples we've set strings as content for Widgets. You can also use special objects called renderables for advanced visuals. You can use any renderable defined in Rich or third party libraries.

    Lets make a widget that uses a Rich table for its content. The following app is a solution to the classic fizzbuzz problem often used to screen software engineers in job interviews. The problem is this: Count up from 1 to 100, when the number is divisible by 3, output \"fizz\"; when the number is divisible by 5, output \"buzz\"; and when the number is divisible by both 3 and 5 output \"fizzbuzz\".

    This app will \"play\" fizz buzz by displaying a table of the first 15 numbers and columns for fizz and buzz.

    fizzbuzz01.pyfizzbuzz01.tcssOutput fizzbuzz01.py
    from rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass FizzBuzz(Static):\n    def on_mount(self) -> None:\n        table = Table(\"Number\", \"Fizz?\", \"Buzz?\")\n        for n in range(1, 16):\n            fizz = not n % 3\n            buzz = not n % 5\n            table.add_row(\n                str(n),\n                \"fizz\" if fizz else \"\",\n                \"buzz\" if buzz else \"\",\n            )\n        self.update(table)\n\n\nclass FizzBuzzApp(App):\n    CSS_PATH = \"fizzbuzz01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield FizzBuzz()\n\n\nif __name__ == \"__main__\":\n    app = FizzBuzzApp()\n    app.run()\n
    fizzbuzz01.tcss
    Screen {\n    align: center middle;\n}\n\nFizzBuzz {\n    width: auto;\n    height: auto;\n    background: $primary;\n    color: $text;\n}\n

    FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u2503Fizz?\u2503Buzz?\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502buzz\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"guide/widgets/#content-size","title":"Content size","text":"

    Textual will auto-detect the dimensions of the content area from rich renderables if width or height is set to auto. You can override auto dimensions by implementing get_content_width() or get_content_height().

    Let's modify the default width for the fizzbuzz example. By default, the table will be just wide enough to fix the columns. Let's force it to be 50 characters wide.

    fizzbuzz02.pyfizzbuzz02.tcssOutput fizzbuzz02.py
    from rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.widgets import Static\n\n\nclass FizzBuzz(Static):\n    def on_mount(self) -> None:\n        table = Table(\"Number\", \"Fizz?\", \"Buzz?\", expand=True)\n        for n in range(1, 16):\n            fizz = not n % 3\n            buzz = not n % 5\n            table.add_row(\n                str(n),\n                \"fizz\" if fizz else \"\",\n                \"buzz\" if buzz else \"\",\n            )\n        self.update(table)\n\n    def get_content_width(self, container: Size, viewport: Size) -> int:\n        \"\"\"Force content width size.\"\"\"\n        return 50\n\n\nclass FizzBuzzApp(App):\n    CSS_PATH = \"fizzbuzz02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield FizzBuzz()\n\n\nif __name__ == \"__main__\":\n    app = FizzBuzzApp()\n    app.run()\n
    fizzbuzz02.tcss
    Screen {\n    align: center middle;\n}\n\nFizzBuzz {\n    width: auto;\n    height: auto;\n    background: $primary;\n    color: $text;\n}\n

    FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Fizz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Buzz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    Note that we've added expand=True to tell the Table to expand beyond the optimal width, so that it fills the 50 characters returned by get_content_width.

    "},{"location":"guide/widgets/#tooltips","title":"Tooltips","text":"

    Widgets can have tooltips which is content displayed when the user hovers the mouse over the widget. You can use tooltips to add supplementary information or help messages.

    Tip

    It is best not to rely on tooltips for essential information. Some users prefer to use the keyboard exclusively and may never see tooltips.

    To add a tooltip, assign to the widget's tooltip property. You can set text or any other Rich renderable.

    The following example adds a tooltip to a button:

    tooltip01.pyOutput (before hover)Output (after hover) tooltip01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\n\n\nclass TooltipApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"Click me\", variant=\"success\")\n\n    def on_mount(self) -> None:\n        self.query_one(Button).tooltip = TEXT\n\n\nif __name__ == \"__main__\":\n    app = TooltipApp()\n    app.run()\n

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    TooltipApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/widgets/#customizing-the-tooltip","title":"Customizing the tooltip","text":"

    If you don't like the default look of the tooltips, you can customize them to your liking with CSS. Add a rule to your CSS that targets Tooltip. Here's an example:

    tooltip02.pyOutput (before hover)Output (after hover) tooltip02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\n\n\nclass TooltipApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Tooltip {\n        padding: 2 4;\n        background: $primary;\n        color: auto 90%;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"Click me\", variant=\"success\")\n\n    def on_mount(self) -> None:\n        self.query_one(Button).tooltip = TEXT\n\n\nif __name__ == \"__main__\":\n    app = TooltipApp()\n    app.run()\n

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    TooltipApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/widgets/#loading-indicator","title":"Loading indicator","text":"

    Widgets have a loading reactive which when set to True will temporarily replace your widget with a LoadingIndicator.

    You can use this to indicate to the user that the app is currently working on getting data, and there will be content when that data is available. Let's look at an example of this.

    loading01.pyOutput loading01.py
    from asyncio import sleep\nfrom random import randint\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass DataApp(App):\n    CSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n    }\n    DataTable {\n        height: 1fr;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n        yield DataTable()\n        yield DataTable()\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        for data_table in self.query(DataTable):\n            data_table.loading = True  # (1)!\n            self.load_data(data_table)\n\n    @work\n    async def load_data(self, data_table: DataTable) -> None:\n        await sleep(randint(2, 10))  # (2)!\n        data_table.add_columns(*ROWS[0])\n        data_table.add_rows(ROWS[1:])\n        data_table.loading = False  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = DataApp()\n    app.run()\n
    1. Shows the loading indicator in place of the data table.
    2. Insert a random sleep to simulate a network request.
    3. Show the new data.

    DataApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    In this example we have four DataTable widgets, which we put into a loading state by setting the widget's loading property to True. This will temporarily replace the widget with a loading indicator animation. When the (simulated) data has been retrieved, we reset the loading property to show the new data.

    Tip

    See the guide on Workers if you want to know more about the @work decorator.

    "},{"location":"guide/widgets/#line-api","title":"Line API","text":"

    A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the line API.

    Note

    The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin DataTable which can handle thousands or even millions of rows.

    "},{"location":"guide/widgets/#render-line-method","title":"Render Line method","text":"

    To build a widget with the line API, implement a render_line method rather than a render method. The render_line method takes a single integer argument y which is an offset from the top of the widget, and should return a Strip object containing that line's content. Textual will call this method as required to get content for every row of characters in the widget.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV/2Vlx1MDAwNe109zxTtXVcdTAwMGJIIISER4BcdTAwMTBya4tcdTAwMTK2bFx1MDAxNORcdTAwMDe2eG7lv99cdTAwMWWHWPJDxjY2ce5d71x1MDAwNowk2+OZc7pPP0Z/v1haWk7vmtHyq6Xl6LZcdTAwMTQmcblcdTAwMTXeLL/0x6+jVjtu1PlcdTAwMTR2/m43rlqlzpXnadpsv/rjj1rYuojSZlx1MDAxMpai4DpuX4VJO70qx42g1Kj9XHUwMDExp1Gt/W//cyesRX82XHUwMDFitXLaXG6yXHUwMDBmWYnKcdpoff+sKIlqUT1t87v/h/9eWvq78zM3ulZUSsN6NYk6L+icylx1MDAwNqjR9Vx1MDAxZt1p1DuDtWhJkJa6e0Hcfs1cdTAwMWaXRmU+W+EhR9lcdTAwMTl/aFndXHUwMDFlnVx1MDAxY16fNnfM0d398cmhkqdlzD61XHUwMDEyJ8lBepd8n4mwdH7Vyo2pnbZcdTAwMWFcdTAwMTfRcVxcTs/5vOw73n1du8GTkL2q1biqntejtv/+0D3aaIalOL3zX0J0XHUwMDBmfp+DV0vZkVv+a0VcdTAwMDVOklx1MDAwMuuUMMZcdJd9sn89KFxurFJGgUGhrXR941pvJLxcdTAwMTI8rt9UhcolmY3sLCxdVHl49XL3mrRcdTAwMTXW282wxeuVXXfz8I3JUICCXHUwMDAwVffUeVx1MDAxNFfPU1x1MDAwZiPUgVLOKtLKOevQZMOIOstcdTAwMDFaXHUwMDFhRONM9u38hze3ylx1MDAxZGT8lZ+wevlhwupXSZKN159404+mPKJyK920Z9VduFx1MDAxMVxmiFx1MDAxOGpn764qm4er3e/UXHUwMDAzv7DVatwsd898e3iWjeiqWVx1MDAwZdOHL2GURGLgWYvd80lcXL/oXHUwMDFmbNIoXWQw7Fx1MDAxY/32clxu+IOAQvzzSljhQMmx8Z9cXLy7PD1vbev9N3uJhlx1MDAwYrmLd1x1MDAxYlx1MDAwNfgvtVx1MDAxYe32ynmYls6LOIAz4lx1MDAwMIhHSeBE4LRgiFx1MDAxYqFcdTAwMDXwXHUwMDFh9JDAuMA5XHUwMDA3vCokrdVAhSRcdTAwMTCdx/QkUFJcdTAwMDaakCRcdFx1MDAwNVx1MDAwMEOoQFpcdTAwMDRcdTAwMTaYplKTpy3qXHUwMDAxKpBTXGYkUnZmVFx1MDAxOFx1MDAwMVaDRln9LGA1aFx1MDAwYrHqnCG2XGZkx1x1MDAwNqvaxvVSZecmxNXjk1x1MDAxYqrCadIuMtZ9gPt5MIWA0JA2glx1MDAxNFx0hFx1MDAwMZhK6yRcdTAwMWFUxlhw84QpXHUwMDA1XHUwMDAytEKrXGaw0bWDOEVcdTAwMWJoLZgvWqAmxvQgTlx1MDAxZCn2KFx1MDAxYWZnslx1MDAxZsOpmVx1MDAxNU6jJImb7eGKQkNcdTAwMTFKnbcsykpcdTAwMWNcdTAwMWKk+1uV6+279t37xsV++nqrtd3ePL+cXHUwMDA2pPB8IPUwJJBsw7RcdTAwMDX2IL0gtS5QhtFAiEJYJfqFzkQg/a1cdTAwMTIqVDhcYlCgQFxuROU08i822nJcdTAwMTChgIHSWjJcbkEgXHUwMDExylx1MDAwMVEh2bxaySrwf1xuoCbnVvpcdTAwMDCK1pFGY8dcdTAwMDcolurV++Rz+aB+bj/o9kH49cRcdTAwMTS5/EVcdTAwMDGo4oVcdTAwMTdCXHUwMDFha0AjsLPsXHUwMDA1qFxyXHUwMDE4XHUwMDExvFx1MDAxNk4oyVx1MDAwMNZPXHUwMDA06JlcdTAwMTBqXlx1MDAwMCW2v0Yw1Z5ccqD2OVx1MDAwMKpcdTAwMGI1qXKs0YhyjHxcZqDHe1x1MDAxN/d7n87XXHUwMDEy8UnvbO2+PvhcdTAwMWNcdTAwMDItOEBRXHUwMDA20kipXHUwMDE0aFx1MDAwMM1cdTAwMTBQvVxiNYFcdTAwMTGCueqIp4qVwJNcdTAwMTBcbnjGmnZeXGLl8TlhrNC/XHUwMDFlQkdqUetcbr08XHUwMDAwSZBKuPFBWj7Y3CuX3lx1MDAxZlx1MDAxZX01N1dcdTAwMWZut1Vauf70XFwgXHUwMDE1U4FUXHUwMDA0QILYO3JULpSPm7BcdTAwMDekoFxcXHUwMDAw0jlEXHUwMDA2MVx1MDAxOYbIk1BqnFx1MDAxMlx1MDAxNcQhrt5cdTAwMDdEikNcdTAwMDNyUqNcdTAwMWOePtBcdTAwMWO/SZSKjFx1MDAxNPwzXHUwMDA35Vx1MDAxZoZUasPhnZ5cdTAwMDSmWVrgXHUwMDA3ZOjhyLdi9HZfMySp8PHy+uaotH178WXjrb7/fLe+d4234yVcdTAwMTVejnrfuSYrXGZcdTAwMWJkSbPiXFxcdTAwMWHdpsPohrYw9EMjrVx1MDAwNDOBSzhxXHUwMDFm31x1MDAxZidcdTAwMDdvjqp2rXK1trL1QZVcdTAwMGWnS9M9o1Mgli0sSViyoPWhXHUwMDE39NFcclx1MDAwMz5swElcdTAwMDFI4Pqj0tlcdTAwMDV/mMuOZFx1MDAxNFP9lGLyK+/DZmj650SeR0HOVoVtuppcdTAwMDDkXHUwMDE5mFx1MDAxYfX0IL7vXHUwMDAwVfRcdTAwMWPdXGJrcXLXg4dcdTAwMGX6X3Vmulx1MDAxYaVcdTAwMDFPfzlqnfKnRb/f/Sn+lV+yduRcdTAwMGb7V9uel68mcdVcdTAwMTNmOYkqvUxK41KYdE+njWZ2tsTDXHT57Vpb5f6v1WjF1bhcdTAwMWUmh49cZm0qVlNxqFxmXHUwMDFjXHUwMDBikdMwfvJRXHUwMDFklsT2znFsWnt2o11cdTAwMGJ33ea7rV+C1Up4SrNzXCKle4NlNq1cdTAwMDEosFx1MDAxNkmwi2NcdTAwMTU2P1ZcdTAwMGZLNlx1MDAwZbKa1Vx1MDAxY0p2o5l5mSepL0vr7urg8qDkbPJ2O6xcXGxdfjibXHUwMDE5qVnTimzZflxuqWFxSVxyU5JajojehHLkI5qxSZ2YKt6n+9H1p9219fhzJV7Zw5tfgNQqQMGKmONcdTAwMTFcdTAwMTamprekhopcdTAwMDJhQLP168R4pm9gs/TUQ2K2IZ6anOPh2GdcInWj9CVsnVTfXHUwMDFlxXs7d+5g/8Jd7u/OjtSan/1cXFLj4pJcdTAwMWFcdTAwMWYh9fdcdFx1MDAxZsJqsLk0Wb+vJiU5XG5cdTAwMTg/aThaqy0qq1lWM29cdTAwMWRcdTAwMWFAzf6YSd6rwDWfVtrn45RcdTAwMDUgnJ9cdTAwMDJcdTAwMDewgVWGPOJRO8rlKLPUXGZcdTAwMDRoXHUwMDA1x2eWQ3Difznp8KNkzr5cdTAwMWOJ9CS0XHUwMDFmXGZ68eHIiKB3XHUwMDE0X0lLmspcdLfTsJWuxfVyXFyv9lx1MDAwZeyhJWRrjGCvw/DSlVx1MDAxZuWKXGKUlZJNtjI8JG0zseVnJmzyNTqwqKzzVXOSzFxioVx1MDAwNr480+3xQX1U5eZXiCvq6HiXPtRcdTAwMGWqq9VwtWBQXHUwMDA2wPB/1pLjNVdcdTAwMDNjXHUwMDAyXG5cYqWUwvjAz6EzXHUwMDAzY0rCdrreqNXilOd+r1x1MDAxMdfT/jnuTOaqJ/95XHUwMDE0XHUwMDBlmFx1MDAxNv5O+XP9VqLp37HX7GfPljJcdTAwMWF1/ug+/+vl0KuLsf39bD+qs/d7kf89uYFcdTAwMTNS9Fx1MDAxZv5h4CRcdTAwMThfUp+guDxauC6uhbNcdTAwMDFLe9ZcdTAwMDO+PFwiOf7qXHUwMDE1LiBcdTAwMDOn0WhpNIl8d8LMXHJcdTAwMWPKwFdg0DphhVOZQ8/6IJitvCp8gXS+ijOsJUghaWEmalx0mrl94zHgJL08k9q30WFvzpSIXHUwMDAwhFx1MDAwNuGs9Wl9XHUwMDBlrVx1MDAxNeSu+m5LePk77S+KULMxXHUwMDE5/OpjWbeTnd23qzWx6u7bZ7T1+vBetdebw4eEmlx1MDAxOExaW7Ik+HPNwJBAXHUwMDA0JFx1MDAxNS8w+1XprFx1MDAwNPdrm7dcImT7x8ogqGdm3vKxwEDzXGbrXHUwMDE29M1EY9u30Vx1MDAxYX5x7Vx1MDAxYrJEQ1x1MDAwNGRWSqFtb1x1MDAwZVx1MDAxNTXbN+NcdTAwMWJalCAgkvOLzNin+5KEVk5LadlMXHLpoFx1MDAwMVx1MDAxNVx1MDAwMFx1MDAwM1x1MDAwNSQrXHUwMDEydJZoiIWz1lxuMmaSTq+ZW7ipI64xLdzoXHUwMDFjQI85YelGzCpjkWWEM4NcdTAwMDbO16TYhnhcdTAwMDPoW+zUYMVmLFx1MDAwYrd1e5rcrVSPcUW3PsLnzXfNK7k3fEhsctF6YUasZyzpIUNcIoau0lxuoNP++ovbt0Jg+8fKIKYnNHCFWSco7lxyRI7IWDVOXHUwMDEyoI7U51x1MDAxM5m3wu7Actg+j2bZyc3YXHUwMDE2qHjWmY/ev/dFp8h+lGkhnWKZl/dcdTAwMDYzzzrl2oeyrFM21q7xXHUwMDEyXGZcdTAwMDRHM+yu6mJptkXQl6Ped751J2JdlKVTp8tmge452s1mZaWGXHUwMDFm2ayDtFx1MDAxNTd//087qvopfbnUfVx1MDAxMlx1MDAwNMFfXHUwMDA1SS3d8y7zTmo9MsKRpmNkP4fLtSn221x1MDAwZtZGPvKewH7EryVtv0nCS6rJXHUwMDBioq9cdTAwMWI3t7VcInm0IP1cdTAwMWMrXCKQaIT1qlx1MDAxM1xmWtvfXHUwMDE3XHUwMDA3Tlx1MDAwN1xuXHUwMDA0XHUwMDBiVO8/zdOa4Ofdz+FNvlx1MDAwNtLuacroiVxyXHUwMDFk8yxfkW9cdTAwMDM3z9PsxIFjYfLXi+lOz8H4RZ1K+0vdnF9cXH84PLtf+1irt+tuM1l4dqBwznI4L1x1MDAxOXhGqL6aXHUwMDBlSfa+TivB/6N4Ws/onLnB4ouv6Nm18Vx1MDAxM7gxzypcdTAwMTD5lqBcXNpnTk1JJFx1MDAwYiud2vq9RGqCRurRSZNcdTAwMDWVnDogjUL4rVNWXHSw/d1cdTAwMGJcIjDo87iW8WjNXHUwMDFjXHUwMDBinWNKTuLoz29cdTAwMTN6XHUwMDE2xTlX62+F31fzjzKciTIs7k8q7jpcdTAwMTSKpFx1MDAxNdaNnzNcdTAwMWKdNFhQhqtcdTAwMDBcdTAwMTUq0dnAxXymfrdnXHUwMDAyXHJW++SF1ajnV1x1MDAxMVx1MDAxOJPhljQoXprniSnn6sNcdTAwMThcXEr/XHUwMDEz+82X4a44Ka6N76PNV8dcdTAwMWYjOJhcdTAwMWS92fq0cXK11npT3r9d/bJn381U146i9zBh+yi9/T5O5dCXu5Umlvn9/Fx1MDAwZVgtXHUwMDEyKVx1MDAwZfeU0UZcdTAwMTdcdTAwMGLbMbb/j1x1MDAxMLaghm0hXHUwMDFk7FRija2cmSysW9Rd/lJcdTAwMTgzVWVwqk6l90yypdW9raXjTl/QXCJ0KPVcdTAwMGZpJINcdTAwMGIrW7r4XHUwMDBlXHUwMDA2KKRcdTAwMDSfa1x1MDAxZb+LePSSz3lX+FRcdTAwMTT2XHJcdTAwMTOGY0Gyxlx1MDAxN4Ogd1x1MDAxYlx1MDAwZVx1MDAwN4zOXHUwMDE3iDhe1Fx1MDAxMmGEh35cdTAwMWGBpVx1MDAwYoS0vqeRQ1x1MDAwMVx1MDAxNlx1MDAwNEPobGWgrZagJZtcdTAwMTOgXFz+6MF3I1x1MDAwN21mlvtcdTAwMDVGsk/lXHUwMDBiXHUwMDAys69ajXZcdTAwMDZLPUVwYVx00Vx1MDAxOFx1MDAxMKT81FHuqodcIjhcdTAwMDbGMZitXHUwMDAzniRp7WCPz1h1q9H7zHpcdTAwMDYloNM6gdJcdTAwMDdcdTAwMWHo01x1MDAxZYOjooC1oUFeclx1MDAwNpiU4tcuXVx1MDAxNULYP/rBm73Zi/zv6dJrVHxjXHUwMDBi35BmnZ5AhVT3bk72t1blUUlhfFgtl0qfP5ZnqkImtGDqUVx1MDAwYlx1MDAxNiAozfAxvu8kn9TsaFx1MDAxMCNcdTAwMDLpXHUwMDA0XHUwMDBiXHUwMDE0R8hRRv+wZpZbU96OknWK/OpcdTAwMDNcZuudfjS5ZslqY1xyPq2ncpFza7xcdTAwMDIzvOFLkTBXuW6HPkpcdTAwMTAvgGOvP75T39i5rq2vJPK02Tyrne5v7X51Z5s/OfKWj0rzwJBjny4sXHUwMDE4Ry5/XHUwMDAzle+0kFx1MDAwMet2sEjOXGIj5rjfb9x6rtTkd8o9z81cYizHXHUwMDAyk2w7/Sc6ntY7gSws/liw3v+Pn1x1MDAwM/uyefYubFx1MDAxZdy//Xik11x1MDAwZeMvd9vVm2e7XHUwMDFiw1TOybeFS/Y9yMFcdTAwMDSgXHUwMDEykEv6d7BcInRgWFx1MDAxM1xinlx1MDAwN806ySx0XVx1MDAxNFhnWpA0/43uI6sz/j4z8/YgUlx1MDAxNXpcdTAwMTDQYNCx3lx1MDAxON+FvK9cdTAwMWbbo1J8vH5v65XD083W12azseguhFx1MDAwMoaLUuyzfVecXHUwMDEzvVx1MDAxYlZcYjj08Dd11PyDr/rpXHUwMDBlRFx1MDAxOVL+plGz24Y20oFImKhE+I9cdTAwMDOZ2oFcdTAwMTTfY1x1MDAxNaThZfftx2MzcVVUrrd0RUNyrfZKXHUwMDFm4q9n9rqojPIsLuRRXHUwMDFh+lx1MDAxNFxmXGJGtkO/P6xv31x1MDAxOElcdTAwMGWjjXaC40q0+Z7kXHUwMDA19Fx1MDAxZlx1MDAxY4uSr3z9XFz34UvN83dcdTAwMWaFXHJhKCx6XHUwMDFkYMbXPXftjav7hjsyNqkmZXO6oe7fvVl098FoIHKotZ9vwVx1MDAxZaJcdTAwMWa4Olx1MDAxMP5mR75Q5XdcdTAwMGL99Po+XG7lt1SoXHUwMDE5llx1MDAwN0Z6XHUwMDEwjrqeelOQ/ztcdTAwMGby4iG5sFx1MDAxYzabXHUwMDA3Kc9o10LwWsXlh2nJPmf5Oo5u1obdWa/z8O/aobbnUNQxN99efPsvXHUwMDFht7uQIn0= widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])

    Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code:

    checker01.pyOutput checker01.py
    from rich.segment import Segment\nfrom rich.style import Style\n\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\n\n\nclass CheckerBoard(Widget):\n    \"\"\"Render an 8x8 checkerboard.\"\"\"\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        row_index = y // 4  # A checkerboard square consists of 4 rows\n\n        if row_index >= 8:  # Generate blank lines when we reach the end\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2  # Used to alternate the starting square on each row\n\n        white = Style.parse(\"on white\")  # Get a style object for a white background\n        black = Style.parse(\"on black\")  # Get a style object for a black background\n\n        # Generate a list of segments with alternating black and white space characters\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(8)\n        ]\n        strip = Strip(segments, 8 * 8)\n        return strip\n\n\nclass BoardApp(App):\n    \"\"\"A simple app to show our widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard()\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp

    The render_line method above calculates a Strip for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard.

    You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those.

    "},{"location":"guide/widgets/#segment-and-style","title":"Segment and Style","text":"

    A Segment is a class borrowed from the Rich project. It is small object (actually a named tuple) which bundles a string to be displayed and a Style which tells Textual how the text should look (color, bold, italic etc).

    Let's look at a simple segment which would produce the text \"Hello, World!\" in bold.

    greeting = Segment(\"Hello, World!\", Style(bold=True))\n

    This would create the following object:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1aW1PbRlx1MDAxOH3nVzDuSztcdTAwMTM2e790ptOhJFx1MDAxNEIuUEJoaTuMsNa2gixcdTAwMTlJ5pJO/nu/XHUwMDE1RpIl7CBjUqd+XHUwMDAwe3el/Xb3nPNdpH/W1tc72c3Idn5cXO/Y665cdTAwMTdcdTAwMDZ+4l11nrn2S5ukQVx1MDAxY0FcdTAwMTfNf6fxOOnmI1x1MDAwN1k2Sn98/nzoJec2XHUwMDFihV7XossgXHUwMDFke2Gajf0gRt14+DzI7DD92f1961xy7U+jeOhnXHQqJ9mwfpDFye1cXDa0Q1x1MDAxYmUp3P1P+L2+/k/+t2JdYruZXHUwMDE39UObX5B3lVx1MDAwNirJ661v4yg3lkhFhGRcdTAwMWHLYkSQvoD5MutDd1x1MDAwZmy2ZY9r6vAjuvlhK+7ffPQvtlx1MDAwZcJNSo7CP8ppe0FcdTAwMThcdTAwMWVmN+HtVnjdwTipXHUwMDE4lWZJfG6PXHUwMDAzP1x1MDAxYrjZa+3Fdb6XXHUwMDBlwICiO4nH/UFkU7dcdTAwMDO4aI1HXjfIbtyNcNl6u1xy1XHX7pBcZkZSca41Zsatueh11zNKXHUwMDExx0ZLyZRcdTAwMTCMypphW3FcYmdcdTAwMDGGfYfzT2nZmdc974N5kV+MyVx1MDAxMi9KR15cdTAwMDInVo67miyZXHUwMDBiiqQglCqMXHKunszAXHUwMDA2/UHmjosgho1SynDMsJCiNMbmp0JcZnN2XHUwMDEyo4tcdTAwMWVnwmjXz1x1MDAxMfJ3ddtcIn+ybXeQKUHDJi2fy8W48S/rYKtcdTAwMDKugoN3e0dXxmNDtn/iXHUwMDFk7uyeblx1MDAxZqTvVLHgKXR6SVx1MDAxMl91ip7Pk2+loeOR791cIlx1MDAwZVx1MDAwMCk4XHUwMDExWlFDTdFcdTAwMWZcdTAwMDbROXRG4zAs2+LueVx00rXKStqxg1a2scZcdTAwMGVcdTAwMDBcdTAwMGLRilx1MDAxOf5gclxcnrw7vd5S9rV93dvjsYjt/lx0WYxcdTAwMWN0XHUwMDE2OdJcdTAwMTgk4n5ukFx1MDAwNblhkFwiXFxzXHUwMDAw1TQtXHUwMDE4QVhSMZNcclpSa7qLs4FiiaQhXHUwMDFjJuHGfVSTXHUwMDBlgiFtpm2744HEmlxuzivn80Q8mIdUXHUwMDAxu8SWhdTMXmf3gnQmRlx1MDAwNTVcZnSLPlxcwI9Aen57b7btx4R9XHUwMDE0Nt7ZXHUwMDBlg8GSXHUwMDA1fOlcdTAwMThlRFwirakmtFx1MDAwNlH+dFpNKtJbwJHyOlxmXHKmXHUwMDA0XHUwMDBiptqgcFxuXHUwMDFmrWT3evx7cPkq+nBp9/ax2Lu+Or5cdTAwMWPsLUl2OWfaXHUwMDE40lx1MDAwMswlauIoO1xmPtmc1FOt294wXGLzoyqac5SDgX91dmxcdTAwMTjGz9aP4yT0/+pUjyq1MHvtdu66zTDoO0Z0QtubpkpcdTAwMTZA8FR0Z/Go7O2CXHUwMDFkXHUwMDFl3C7Z9evriZOgXHUwMDFmRF74fpZNi3tcdTAwMTbGVL31jrWGXHUwMDEwIK3hXHUwMDBmZ+3u3ubbbSrVxYuD991cdTAwMWXTn3x/J5jB2lx1MDAxYfv+K79cIihGVIBCmqZjwVxiT7XWucsk73XVIzxcdTAwMGJhSNx5ldaeRTpBxZqV6/m/XHUwMDA1WMYwgkvFelwit0VmM4AoyjGRpEVwdfM2ftU/O9u/XHUwMDEx4uJ0g1x1MDAxZI9e71x1MDAxZmyuuuNcdTAwMTJMI6yVXHUwMDEyhlPnKNg0XHUwMDExuECSc8HInCjr0X5Mmyb4m35MSaG5Ulx1MDAxNU16Sj/2q/fJo3tcdTAwMDO6uSuOduTxsWTnLy+Wlj5cdTAwMTjNOG2B7sf5sVx1MDAxYz3fn8Wh/9P7ZGx/WFx1MDAwNT/WsGmxuLNidZ3AnEHKXHUwMDAwaWhcdTAwMTmafonAL0/UwcvRfnK+8bt9dbQpe1x1MDAxYofh6apcdTAwMTOYUYYoJNzwMVxuXHUwMDEy93K57nouOVx1MDAwMvpSXHUwMDE3PHHYjbp3XVwii+k9Low2XFyXkka6QK6N61rNaFRcdTAwMTBcdTAwMTBF9lQsJlNsdFx1MDAwNvZcdTAwMTNrsyDqo2kyVChcXIH616DwtEGL8Vx1MDAxN8/kL/gjXHR7rMpcdTAwMDFfou9V/8Ppm1x1MDAxNyPpXHUwMDFmvtl68UZEoThcdTAwMWOfrDp9hYJA0HBiZO6AWZ2+XG5RIDV0XHUwMDAwezWresVl87dCyTn8hWReSY1L2H+zTphcdTAwMTCBK1x1MDAxOfRXo2+aY2mV+Htr0VxcXHUwMDAy327vfUlkpVxmWvfAWlx1MDAwYlx1MDAwMVx1MDAxOU5cdTAwMGJcdTAwMGY8X69XlMJMXHUwMDBiJFx1MDAwNGR04Fx1MDAwZaRcdTAwMDF/PFx1MDAxZENzSZCmmFNtnthcdTAwMDVcdTAwMWKkQElcXKFEaqOwvqc8pDRSICXmLlx1MDAxY6hcdTAwMThzV7yXcFx1MDAwM6ZcdTAwMWVXu6eTlsVqlos71jTzkuyXIPJcdTAwMDHW04ZNnlHtPiDQy7ncXHUwMDFkOys3MMJcXFx1MDAxMVc9g3CSUWp4+WjG7Y03cqtFkuWlv8aqbeR/2Zr5XHUwMDA1z4o1YFxmwVx1MDAwNsPeMEyoNIRy1TCGMMjn8pJGw5rQS7OteDhcZjLY7v04iLL6tub7t+m4PrBeQ0BgNdW+uiiM3Fx1MDAxZKc1vfy2XpIm/1F8//vZvaNnYtl9Nlx1MDAxYTAub7dW/d9azqiaWclcdTAwMDYh4EJcYqVcdTAwMWUuZ/P914rKmWRcdTAwMWNcdTAwMTmqMVMu6IDMoaFmsFx1MDAwNdJhzEUkrGbX8tRcZuJcIsyNUkZcdTAwMGLIb4SsJP6lnCmEXHUwMDFkXHUwMDE1INOrXHUwMDE5M1EzJjXRXHUwMDFjyzZFg2XL2eLZ/lx1MDAwM+VsfuBblzNcdTAwMDaUYiBoXHUwMDEwVzqK8cq4W1x1MDAwNVx1MDAxMYgzdr+CPEjP5tfBpvWMXG7KNVx1MDAwMVx1MDAwNitOsKaGNawhXHUwMDAykVx1MDAxOer6LenZxmw45911JLdcdTAwMTS0WVx0lmGi3lokWFxmpuK0zZO5M9zt9a8hXHUwMDFhXHUwMDFkXHUwMDFmXnw8+qN38mv8epFcdTAwMWH/XCJitth7XHUwMDE1Tq5cdTAwMDTPdUxgXHUwMDAzwVlZpHA3oJQh8KJGXHUwMDEy6TA/O7tcdTAwMDLn7/H5YvZdr9drqph6UFWEQOKHtWStdGrxvOqJS/dSV6pQXyuvWqWMasFcXIpSQurNRfRcdTAwMDFbXHUwMDBiQCUtkqn5p7ySj+S4XHUwMDExiCqmlVCUS1x1MDAxMMKSkTldhUBaQSxuXHUwMDA0eHZiKq/GLI2wXHUwMDEwP3NcZiqtJOdcdTAwMWFCoEq4Vz6ZI65mg4VDPDZcdTAwMTJX9GzygFx1MDAwZbSVske+XHUwMDAw9aioQ2JcdTAwMTeutnlFqW3UMd9cdTAwMWJMuXnKMVx1MDAwM99GKTbCbUwz5lBIuUhAaYWpXHUwMDE0RuNcdTAwMDVjj/lv/9ViXHUwMDBmLMDlau5cdTAwMWVwSaFpwyhcdTAwMDKBMNPgrImGMFx1MDAxMoY0n5h+SyHIbGS7T1x1MDAwM9OzXCKQtclcdTAwMDRcdTAwMWRvNDrMXHUwMDAwdMVxXHUwMDAwylx1MDAwM39cIuDlKjuXgb365X76OVx1MDAwNq5N9tNcdJLNmfB57fO/MTl+mCJ9 \"Hello, World\"Style(bold=True)greeting.textgreeting.stylegreeting

    Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments,

    "},{"location":"guide/widgets/#strips","title":"Strips","text":"

    A Strip is a container for a number of segments covering a single line (or row) in the Widget. A Strip will contain at least one segment, but often many more.

    A Strip is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text \"Hello, World!\", but with the second word in bold:

    segments = [\n    Segment(\"Hello, \"),\n    Segment(\"World\", Style(bold=True)),\n    Segment(\"!\")\n]\nstrip = Strip(segments)\n

    The first and third Segment omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text \"World\". If this were part of a widget it would produce the text: Hello, World!

    The Strip constructor has an optional second parameter, which should be the cell length of the strip. The strip above has a length of 13, so we could have constructed it like this:

    strip = Strip(segments, 13)\n

    Note that the cell length parameter is not the total number of characters in the string. It is the number of terminal \"cells\". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.

    "},{"location":"guide/widgets/#component-classes","title":"Component classes","text":"

    When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining component classes. Component classes are associated with a widget by defining a COMPONENT_CLASSES class variable which should be a set of strings containing CSS class names.

    In the checkerboard example above we hard-coded the color of the squares to \"white\" and \"black\". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the \"white\" squares and one for the \"dark\" squares. This will allow us to change the colors with CSS.

    The following example replaces our hard-coded colors with component classes.

    checker02.pyOutput checker02.py
    from rich.segment import Segment\n\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\n\n\nclass CheckerBoard(Widget):\n    \"\"\"Render an 8x8 checkerboard.\"\"\"\n\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        row_index = y // 4  # four lines per row\n\n        if row_index >= 8:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(8)\n        ]\n        strip = Strip(segments, 8 * 8)\n        return strip\n\n\nclass BoardApp(App):\n    \"\"\"A simple app to show our widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard()\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp

    The COMPONENT_CLASSES class variable above adds two class names: checkerboard--white-square and checkerboard--black-square. These are set in the DEFAULT_CSS but can modified in the app's CSS class variable or external CSS.

    Tip

    Component classes typically begin with the name of the widget followed by two hyphens. This is a convention to avoid potential name clashes.

    The render_line method calls get_component_rich_style to get Style objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.

    "},{"location":"guide/widgets/#scrolling","title":"Scrolling","text":"

    A Line API widget can be made to scroll by extending the ScrollView class (rather than Widget). The ScrollView class will do most of the work, but we will need to manage the following details:

    1. The ScrollView class requires a virtual size, which is the size of the scrollable content and should be set via the virtual_size property. If this is larger than the widget then Textual will add scrollbars.
    2. We need to update the render_line method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.

    Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.

    checker03.pyOutput checker03.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\n\nfrom rich.segment import Segment\n\n\nclass CheckerBoard(ScrollView):\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\n\n    def __init__(self, board_size: int) -> None:\n        super().__init__()\n        self.board_size = board_size\n        # Each square is 4 rows and 8 columns\n        self.virtual_size = Size(board_size * 8, board_size * 4)\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        scroll_x, scroll_y = self.scroll_offset  # The current scroll position\n        y += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\n        row_index = y // 4  # four lines per row\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n\n        if row_index >= self.board_size:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(self.board_size)\n        ]\n        strip = Strip(segments, self.board_size * 8)\n        # Crop the strip so that is covers the visible area\n        strip = strip.crop(scroll_x, scroll_x + self.size.width)\n        return strip\n\n\nclass BoardApp(App):\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard(100)\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp \u2585\u2585 \u258b

    The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the virtual_size attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.

    The render_line method gets the scroll offset which is an Offset containing the current position of the scrollbars. We add scroll_offset.y to the y argument because y is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.

    We also need to compensate for the position of the horizontal scrollbar. This is done in the call to strip.crop which crops the strip to the visible area between scroll_x and scroll_x + self.size.width.

    Tip

    Strip objects are immutable, so methods will return a new Strip rather than modifying the original.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXNcIkmS/d6/QlbzcZuccI97zMbWdKFcdTAwMTPd6GB3TMYpkFKAOCSktv7v645UXCI5XHUwMDEyoYvKYtVtqiqSI4h87v6eh4fHX38sLf3oPDbLP/619KPcK+bDWqmVf/jxJz9+X261a406XcL+v9uNbqvYf2a102m2//XPf97mWzflTjPMXHUwMDE3y8F9rd3Nh+1Ot1RrXHUwMDA0xcbtP2ud8m37v/n3Xv62/O9m47bUaVx1MDAwNYNcdTAwMGZJlUu1TqP1/FnlsHxbrnfa9O7/Q/9eWvqr/zsyula52MnXr8Jy/1x1MDAwNf1Lg1x1MDAwMVx1MDAxYWVHXHUwMDFm3WvU+4NcdTAwMDVcdTAwMTRotdBavj6j1l6jz+uUS3S5QmMuXHUwMDBmrvBDP453K1AvX/dcdTAwMGWLK6nWib/e1M3LjcHHVmpheNx5XGafp1wiX6x2W5FBtTutxk35rFbqVPnTR1x1MDAxZX99XbtBszB4VavRvarWy+320GtcdTAwMWHNfLHWeaTHpHh98HlcdTAwMTL+tTR4pEf/SjlcZjRcdTAwMDJ4kFx1MDAwZZXE14v8auVcXKC0tVJIROE1yJFhrTZCulx1MDAxMzSsf4j+z2BghXzx5opGVy9ccp6D6FxuZTN4zsPLlzUmXHUwMDEwylqtUGlnhFevz6iWa1fVXHUwMDBlPcVi4KQ2wlx1MDAxOIHCOTdcdTAwMThnu9y/XHUwMDFmXGJSKbruXHUwMDA3N5Q/vrlV6mPjP9FcdTAwMTmrl15mrN5ccsPBiPnCelx1MDAwNE+D13SbpfzzbVx1MDAwN2O1Rk9DoLl5vVx1MDAxZdbqN6NvXHUwMDE3Noo3XHUwMDAzpPRcdTAwMWb9+89cdTAwMGZAlO5KXHUwMDFjRK1cdTAwMDaNXHUwMDAy3OxcYl3LVm6ue8t7d1x1MDAxOXl2eFxcObnPZdeWXHUwMDEzjlBtXHUwMDAy0MpcdTAwMWGp0ErrnDfjXHUwMDE4ldJrXHRcdTAwMDRTacEkXHUwMDE2ozR85b0wesEgaoWLgyhISSZcdORDZsZo+WRzm1x1MDAwNl8olNTRQWbL7excdTAwMWPcqIRjNFx1MDAwNTJcdTAwMTDgnfZgwVx1MDAxOedgXHUwMDE4pNrIQFx1MDAxOSfoXHUwMDEyoFOj40pcdTAwMGVEQUvp6D9YOIjqOIhaa5SQxsLMXGI93y/s7VxcaFdoXHUwMDFjXHUwMDFj3ubP7zeJ37R/LUJBvOlGVWC1oWl3XHUwMDA2vFFgh1x1MDAwMGqkXHUwMDBmiFx1MDAwM3hPXHUwMDFlSmilMbFcYkWwVtFQXHUwMDE3XHUwMDBloVx1MDAxMEtFUSjiNqD97IH+NpXLy3RxOXucq96U16pcdTAwMDfrXHUwMDFiISTdiTpcYoiFKidccjqg71xmw1x1MDAxMFVcIiBcdTAwMGaqtFx1MDAwMkVE1erEQlx1MDAxNCyRXHUwMDE0TTFg0bio9bFcXFx1MDAxNFx1MDAxY01cdTAwMTdFNz27XHUwMDFi9TZvdNi4vu5cdTAwMTazcFx1MDAxOXZLjX27l3CMXHUwMDAy6sBcdTAwMWKhKcxcdTAwMTNcdTAwMDR8RFx1MDAxMD2HeU1cXNRcYutQ9mNKciHafy25lPm5UT9cdTAwMWa5JHxcdTAwMWNEtfDOXHUwMDAy+plcdTAwMDHaetquLtv1PVxym5uynFp7Slx1MDAxZlRPk1x1MDAxZefpzlKgJ1x1MDAwNmdR05/aXHJLesJtgI6UvDGoPGKSqahw4KxcdTAwMTBcdTAwMGJcdTAwMDdRXHUwMDE5n3RykmKbfIdcdTAwMTNdllx1MDAwNd+53Xpyjerm8Xr2XGbyp/WkK/pcdTAwMTRgIDmvRLLeoSBHaUYwqomMXG5E68lFXHUwMDE5n9ysXHUwMDEzgDWk6oWaX6SfXHUwMDBmRq0wcVx1MDAxOPXgPOiogHhcdTAwMGKjK141fKdzs1Jo3+CtKUH7ot5MOEbBXHUwMDEzXHUwMDA2lVx1MDAwNVx1MDAwMUpcdTAwMDLFe1x1MDAxY470ylx1MDAxOYKGoD+EQYr3oJKLUUnwcdZEXHUwMDAy42JglD4pXHUwMDBlo8zMlNJmdozuhL3qWlXmMlx1MDAxZFFcdTAwMGXXzcNj2OydJFx1MDAxY6NSyoCYptBcdTAwMTTKhTM4mrwniFxuY+mOWCFRJDgxSjZGMUBoOb/M6JzcqME4iFwiKE30Rjo9M0b1zXpYKFXu08f3WpRy5+FlfT1MOEbRqEA68j9cdTAwMWWU0TKShntcdTAwMTZMXHUwMDEwOMKuQk5MYYKdKPlcdTAwMTJA4qNcdTAwMGKHUFx1MDAxZJu7d2hQ0q2ZPeu0eqMuOnBdusruXHUwMDE1W6dcdTAwMGYtma3m80lcdTAwMTdM7EWV8kQzXHRcdTAwMDFcdTAwMDCRtYyfWSdA4ywvjoIzXHSGqJVcdTAwMTL418JBVMk4iKKj2dL+XHUwMDFkq0v6+mJze9dm95dVuYHb+6enzZxNOkTBm4Cw4STxXHUwMDFhXHUwMDAxUo5hXHUwMDE0XHUwMDAzLSjWkGJcImbuklx1MDAwYlGyMbpjOMdF+jlRUVx1MDAxZFx1MDAwYlFtke5cdTAwMWH42SU9bi7bo3C7lrtfbV5f7eNtRVxc51x1MDAxMlx1MDAxZeaVkIEha1x1MDAxNN4rXHUwMDBmVo2GeVwiXHUwMDAxSMhcdTAwMTDSeUy0oDfaWWW1XTQn6lxcbF6UvIvxRkaXSN+C6F7n8r6mbnqnzZW73HLnMXN8VF3/XHUwMDFkIGqEJS6qnZMjqXvOOVx1MDAxMVd1gqhcdTAwMDCr/eSKJS6FsVx1MDAxNtTCIVTGMlFcdTAwMDM0a1rI2X3oxe7DRqHaq4Rdd7ZV92erjfr+RdLDPNHMwFiHJJPAOI3KjkDUXHUwMDA2XHUwMDAyXHUwMDFk31x1MDAxOGe4jiixXHUwMDEwReEleVHQi5ZysjLWiyryoUJqNbtcdTAwMTNcci+ecvumqFx1MDAwZm/NQ72zv3N73To/TLhcdTAwMTNNOZJD9D3BSclcdJtcdTAwMTGEelx1MDAxMVgwSoNcdTAwMDaLTiVXLCktkWJeRFksXHUwMDA2QJ1cdTAwMTdxXHUwMDAwXHUwMDA1Vlx1MDAxMHTv9OxcYr16KKZPa21wl9nH7VpVmePc+i9Ois5Q6GRJXGZJ+qaEQZr3KD6eMVxugdJkrFZcdTAwMDOp/YiLSlx1MDAxYUbpXCLxXHUwMDE05Vx1MDAxNm3908ZjlL+wgWhZxZvVou2KKp6c7Oid5mrT5DLd6nphM+lOXHUwMDE0REA6wztcdTAwMDOoud4uUpPAb+BcYj2kokjqk2Am8CS3jMRcdTAwMTlnhV84JlxusVFcdTAwMWWFXHUwMDE2Xoj3rCx5XHUwMDExVjvdwoYt75ZqXCJzXuymdlx1MDAxYlx0Ryj5UGk8ek1cXFR7gcNy3itcdTAwMTF4hSRDXHUwMDE0XG6Z4Fx1MDAxMlx1MDAxMpLxxEXkwiXtnYivXHUwMDE1JXySdphcdTAwMWSehUZcdTAwMTWuWts7t08rXHUwMDA3t1f3ze398/VMwuGZsi6w1pIs1I6IJrnRXHUwMDExfMpAopTOoqbJwORWiqIgLi1cdTAwMTDEwvFQXHUwMDFiW2+PLJSMtbPT0OPMRerk5nxv67540d3aL4dcdTAwMGZcdTAwMGb791x0hyigXHJcdTAwMTR7SFCS5LBcdTAwMWGN8Fx1MDAwNGAgnU86RHlcdTAwMGLJLbe3aLVfQFx1MDAxN+ohNtukpLXo4Vx1MDAxZEp+fcs9XFyd7tQz9+XD7PLKSemyW096OpRcdTAwMDHKy1x1MDAxMp5cdTAwMWOQ5FxyIUNcdTAwMDAl7qlcdTAwMDMhNTlZrZXxNsGLSob/035cdTAwMDG1fHwps1x1MDAwNE9cIvZcdTAwMWRL8/dcdTAwMWJcdTAwMGad86Otq8xarry6fX2xXazmz1x1MDAxM1x1MDAwZdFcdTAwMTS5UMF1MsJpY6xcdTAwMWYpZSaM2sAp+lx1MDAxMUT0SEol14tcdTAwMDJdsDSChcuHmtgw7zQ4XHUwMDE33Y79pk7avYHbTmpzt3l3XHUwMDEwbuyk6j19eZD0ZFx1MDAxM3jL2+eFQCSkRpcwnnNNXHUwMDE4KND9XHJ3hGKdXFylXHUwMDA0QCYk1OIlRH188VxiOO2dXHUwMDE3XHUwMDA2Zlx1MDAwZvS5Wlx1MDAxNcqNVLpWz2RrOrO50fBcdTAwMGa/WMvPUuBkXHUwMDAyQqDXllx1MDAxM0p+dFWJMCqJXHIxepBcdTAwMTdtXHUwMDEyi1FjUJOkc4vmRJ2MdaIgOIXt5DtcbpzOT1x1MDAwZotccn9YujqrXHUwMDFkZU+0bj21Vs5cdTAwMTJcdTAwMWXo0fDKJil2pYiWXHUwMDEy3Vx1MDAxY07ZO7qsSMqT4JdcdTAwMWGT7EXRWuJcIkYvmlpyJjbhpFhcdTAwMWPCe7YsZc+3m1x1MDAxYrWtjaNMa2tXn3SOz07bSW+TI6VcdTAwMGVcdTAwMWNcYqKhxEhp1ofVkldcdTAwMTA4a/t1I+RpXHUwMDEznFx1MDAxMVx1MDAwNcnKwZk59iCZV1x1MDAwNV5spT2QNDBWKJw9zvuV8/rpffWyU8BcdTAwMTW3WXkqrJVcdTAwMWbuXHUwMDEyXHUwMDBlUfAuIDFvhffaaFx1MDAwMGlGMKpcdTAwMDKKn2iIqYJEmVxcQe/J0Kywi0dFMX7fp1x1MDAxMZr3jNvZNydcdTAwMWbs7dhuSp7ByeN1/cGmsVPYLCdcdTAwMWOiSlx1MDAxMDxIXHTT/UUhhVx1MDAxZNlcckJyMTCaVD1xVMNro8mFqDZcbr3Ri1bH7OPT9oCO63tRzVx1MDAwZdH1Qlhv3e1cdTAwMTfSXHUwMDBltleObjZuXHUwMDFl1sKjxEOUXHUwMDAzPVFRYclcdTAwMGaBXHUwMDE4dqL9tKi3gvyr8kbK5Fx1MDAxNo9cdTAwMDCS/Vxit4iCPt6LUtBcdTAwMTDc9WB2Lrpmd+5ztfW1p/AqbNbq4DauNm5cdTAwMTNcdTAwMGVR8p+BIMmOdPspkFx1MDAwYmdHMcqro16C1nR7olQvcSBVvDuVSfOCgVSa+I54zit05Edmz91X7/Nb6V1cdTAwMTf2Llxua2m7nXdHO+W4XHUwMDFhp1x1MDAxMbBcckN0tFx1MDAxZfP1VaV8u1p+XHUwMDA3Ru0sbUVZXHRpZTn1NJq6573JXHUwMDA2eEcl31x1MDAxOTDuUyVOnVa+3m7mW4SBcZxqrVx1MDAwMmOs05ZmXurIkskrTlx0oUG/o5TWRJ9hUs8xxWu5qL5cdTAwMGWnL1x1MDAxN1x1MDAwNsCK3O+Gvdjcv8Dl5ulx+7RSa/m6yVxmmndccqEw32o1XHUwMDFlfrxe+fvPae+bOd+9XHUwMDE2+TOzVz0uioNmoVx1MDAxM+7ordne9+VvybAucDZ+M4vgLXJcdTAwMTbf0c5v+bKS71xcXHUwMDFk7Fx1MDAxZjdSVVkopo+PN1RcXD5iqnmNrj99azM/bblcdTAwMTZNXHUwMDExTyalN8yjjXRcdTAwMDFcdTAwMDV+0CwriG/Hl2HTs/LqjVx1MDAwMFCpVMatSipHI1BI4ZY+30VcdTAwMTbTX62KXCJT4HkvXHUwMDAzyU2DUX3+umHVaMlcdTAwMWJWv45FT7Wq89LpKt5V7vaq+96Yxoo5yJz8juhcdTAwMTdcdTAwMTCf6rA0o85F6/LfQr8tbu7Ui4VO53Hn3pnlp9Ku6vaSvqShdSB5r1x1MDAwNVx1MDAxMoSUVsRyhlxyQEEgyd+DI1x1MDAwNjy0pTtpXGbIglRCLdy6MFxijMcoXHRJ40HNzn+2lrP3XHUwMDBm4U1W5S5219IrWbVij4pJhyh4XHUwMDE1oNFWcl9cdTAwMGIyWTWGUFx1MDAwMVx1MDAwNlx1MDAwNXlp3tGdZI7OXHUwMDA1XHUwMDE4Xi9cdTAwMWNH91x1MDAxMYowuqbRryrRZvZUhzvdyJz27uzmyurxzr06XHUwMDBm5ermL27DNsvCMFx1MDAwNqSYvSdcdTAwMDLurbejPYFcdTAwMTVcdTAwMDaSN1x1MDAxYzpP0TvJzVm80t5TXHUwMDE0WDg3qlxcrFx1MDAxYjVWSVx1MDAxMlc4e6TvPS7fhPlwfTt31MvlTn0h3HWtxPNcXCG04jVGr7mkeyTOXHUwMDEz0SWKS1x1MDAxNJIgqpz93I7YXHUwMDE4omtcdTAwMDOtaFx1MDAxMJKbwGk1QT7CM9tGYl7cKFx1MDAwNqL7dV52XHUwMDFiXG6Dyir4ukW3aURcdTAwMTex97jTbsPxg6rD9uPBtj+utWcjulPl429EoMthWGu2J9tcdTAwMTTxxjibQsKIXHUwMDA2Zdzs4nE1tfm0dbyXlb1cdTAwMDPZvs4101x1MDAwZvthXFxcdTAwMDJxqlHNz+1cdTAwMWKiXHUwMDFlfI5cdTAwMDa3XHUwMDFmXHUwMDEz0c6Fz05fXHUwMDEzMUHJus1+cot5Ja9R47hNsXS0xtHryb+p6IlcdTAwMWSvNuUoKqDtb9Hs96yXoyZcdTAwMDVcdTAwMWVcdTAwMTVyt/D5iMdP2FSSsK+mtPXkXHLsxsvZXHUwMDE3yVx1MDAxZkXtonq/bVx1MDAwMVx1MDAxZXZ6XHUwMDE1n013u+eYbOhbctVAmDFeSEUqeVQ2qn5iXVvlUVxu87l64kq+QN7ke9BvKfJcdTAwMTn9lfnIZMBTx+f1nOZ2rO9pUrPSOzg9Or9q1zOt+onRqdst7dLJxqc3XHUwMDAxeUyJwnpBXHUwMDE4XHUwMDFjQadcZlx1MDAxY0hcdTAwMDOWa+GI9Hxcbp2AXHUwMDA15ybw8a9AJyrlXHUwMDAwo0dcdTAwMDH8Nuh8K/NcdTAwMTa79qiNJOeJcnbJeLeVrm0yc7BcdTAwMTdP9e4y3jzI9C9u8zVcdTAwMDMhN4RBL/uiUWvrRurd+SwuuiOSdJxcIsKb3I3rXHUwMDAwaHjBZ+G6KHlcdTAwMTWfeFPsRH20XHUwMDEx9FtcdTAwMThN27ua9duVxure6nlnuXZb3ln7xZ27Z0trXHUwMDEwj9GOlCNnf4drNblHXHLvXHUwMDE1XCKhJrj1QoJrNWl00jrxO0b56aduxm9sXHUwMDAzT1x1MDAxOFx1MDAxNVx1MDAxNFneUU58mOnKXZu9W1x1MDAxM+srOzv32Vx1MDAxNYzd2fblYV58XGKh2lx1MDAwNki+UlxuirROR49cdTAwMTjtl8FpRVxi9U5JQb/d51LDxXJJlfLjXHUwMDAwlcR1pVx1MDAxNUSB0ViylkhkXHUwMDFiJDYwMF4765ltSHKYalx1MDAxNKLIXUeV/Uo3+nJholxuM2m9d5KRT1ukXdcvU3p143LvcDZcdTAwMTX257T33czVTHhSf+q08+Ji9XBv/ab31PgqdUdwILo+XHUwMDFm51x1MDAxZp/aIOZsXHUwMDE0yvdcdTAwMWPEuFJO5eHo2nWaZ49yZb1X2c2sJZ6gXHUwMDEwOVx0LFx1MDAxYc3HL1x07q0w3v1JXGLD59xcdTAwMTlF/jW5PfRcXL9kxSyc85+6Ic9xu1x1MDAwZfOOKuiLsL0s072101x1MDAwYpHN71x1MDAxYtvJp5dlsp0/qdxcdTAwMDClXHUwMDA04tDcjlSPIJS9v6BcdTAwMTBoiK55Jz/ZcFxcXHUwMDE1TbkyIVx0XHUwMDAx3lx1MDAwN9JbXHUwMDAwj9yHSkyo3yDvT+BFQzpUOOWjNVx1MDAwNT8hXG5cXH6iXHUwMDE27oCmaD/xXHUwMDExiEq2SVx1MDAxZF1cZn1cdTAwMTOhd729w4vl/HZnZ2VHnl2J1XVI/uI16lx1MDAwMKxcInhwP3mMXHUwMDFlY/HcidRcdTAwMDSSXHUwMDBiMFx1MDAxNOk8XHUwMDFmLeBJmlx1MDAxN1x1MDAwNU6kWLt4zSHIaOMwalx1MDAxNUU4mozZI31ntbZXrbWrdrmi0o3Udc2ellx1MDAwYknHKFx1MDAxYVx1MDAxOWiB5JpcZoAzY/2cTUBcdTAwMTSagihywjfBzXKl5lx1MDAxM7VcZs4tXHUwMDEzXHUwMDExab/4rdkygFg2yrkh3nw/O0Srayd+67GweplXXHUwMDA3T83zfPmweFx1MDAxOXcqeEJcdTAwMDK9dC6g6GmRmDc5Slx1MDAxY85DXHUwMDE4TVx1MDAxMVihUJI7zmvxPTJcdTAwMGZkoIhE8mJcdTAwMDFZgvGRj1x1MDAxOVx1MDAwNHqv+Cwzy1x1MDAwZV8hUZLxxTbB8mHo1d8p875Vjs3TXHUwMDAyplfBWURcdTAwMDPvqDG6gHSx2Mvu11x1MDAwZdPiuuuf0ie4XHUwMDFld1x1MDAxY2lC1jPYXHUwMDA0LHeC9rxcdTAwMWRFXHUwMDFhXHUwMDFjWXAjscY9KZSxRDC9/tySRizXhcCQtCBkU5RcdTAwMTAmmltcdTAwMWGsaSA3vFx1MDAxNMJb0d8zM8Z1uaU198iYz3Lz91pcdTAwMDBxh1xiw/2kXHUwMDA1dMq9zkTw2/hcXFx1MDAwNDvDfmO5mbF/WMrc7V9cdTAwMWTpjebVQ+8207kut+1jsrfAXHUwMDEw5lx1MDAwM97agqiJXHUwMDE1Kj16yKly/eI7XHUwMDE0ktgpfvKs6Im1S6gn8JHoyVx1MDAxNi+bWYWWnjuKzVx1MDAwNdrftbnlNH1oiqliPsy1ZK+3msvmunj8y4LGXHUwMDAwmY1657j21K8uckOPpvO3tfBxXGJdfVuiXHUwMDAx3tdanW4+vGzTXHUwMDBig5c7XHUwMDE3uf3tMlxyoP+Oeuily2Htik3vR1iuXGbbZKdGXHUwMDEz83q504is4Vx1MDAxNGkoeXq71lZp9Cs1WrWrWj1cdTAwMWaeTFx1MDAxOdZU3/A80Vx1MDAxM5yDjO9cdTAwMWLG68rkqmdcdTAwMTfY01x1MDAwMfVcdTAwMGLKXHUwMDFh3/RcZlrYgMNcdJLxK1x1MDAxN21o1H+51IHU5Fx1MDAxYVR/Z4P83Fx1MDAwMtXkosZAcDrfSevpfy0mXHUwMDE0NVwi14EpxedcdTAwMWIrjUpGT1x1MDAxNXshhY6rXHUwMDAwXFy0XHUwMDE3cZKES2TW8q3OSq1eqtWv6OLAdfwoP3/01lxmXHUwMDExpm+zxS6PMiVcdTAwMDLPRf2aj6ZWpP5cdTAwMDbHXFzwXHUwMDE05JvPtFuS6lx1MDAwM+2lN8R7lHt5xqtcdTAwMGb7Ua6X3lx1MDAxZdT0nZ/RQUFAZkM3RIO1Rlx1MDAxYTM+Jkk0qK+6pNbWXHUwMDFib8aGXHUwMDE05tud1cbtba1DU3/QqNU7o1Pcn8tltupqOT/mL+grRa+Nmn+T33E4Plxm/rY0sJH+P17//p8/Jz47XHUwMDE1i+H+1XH4XHUwMDBl3vCP6J/vdl3g4lx1MDAwZsxcdTAwMTQ0rzp6XHUwMDE4+Fu+a3rQSqTvXHUwMDAyXGacMoq4hTPK4OjiOlx1MDAwMZ9cdTAwMTNcIkKTojTuk1x1MDAxNXRcdTAwMTNJTUBKQTjuXHUwMDEyQoJcdTAwMTYnkXn0JvBoXHUwMDE0N1j2Q2vJr9VcdTAwMWbgSVx1MDAxMkj8f+a6ROCclyTDvFx1MDAwM7qFkSbdr16CXHUwMDBi6lxy3TuCOadcdTAwMDOMn+64hr/F7+Q/YnHEP2NcYnqn94hcdTAwMTNFXHUwMDE4aVx1MDAxYzbqPKRA8lTvKM5pZFx1MDAxZs9ubaPcLGbO2/uq53Xq9EOFXHUwMDBm89NEzlx1MDAwNzTd5LWVdlx1MDAwZYRcdTAwMTjO2fJOzUDxjk3pnJRcbj/XXmWy+1AzaVwiXHUwMDEwXHUwMDEyrHZfeLTJK4K+dmP+VE10eUfM/na1dLV2SVx1MDAxYXPz5GB/q1hdXHUwMDA0TfR8N5MmiZ5H9TFagT5eXHUwMDExkSP22pnZN8xOx1NcdTAwMTJphVTE5/hcdTAwMDRuzV1cdTAwMDEs4qhnQKJcdTAwMWRcdTAwMTSUXHUwMDE09y6Unz1cdTAwMTJpomdcdTAwMDBhXHUwMDAyR1x1MDAxZu1IgFx1MDAxMmsk/TXuKIhCoyT2Yzw3K3U+kjP5mUyxzlxuObfVxm/lXHUwMDE101x1MDAwM8wrZfBcdTAwMDF6a9FYllx1MDAxZdxcdTAwMDd7smhcdTAwMTKcXHUwMDA146JcdTAwMWPBZbdK/oypXy2Jnlx1MDAwN4UuXHUwMDAwzjtznLGSXHUwMDBmMpqsmoRcdTAwMTe8QEKciIZlzLhO+51ITTyE+Sc1jt530pp474XxR7p5x4fcvKPubHrUSqL3sjJwJHZI+TG43UhCXHUwMDA3+KhccuuE1EZ5bsvyXHLbVEFDXHUwMDAwXHUwMDE0XCJcYsaOTFxm3KR1voC7oDrSqJJbbMrIdtSfzotgQaFmfodf/1r3XHUwMDE1cU5AhFx1MDAxNEi1XCJcdTAwMGIk7uw35lC48Vx1MDAxZlqFkmjhc9Ob6f7r91VG8Vh6vjxcdTAwMDajd3qROHFkpm3MRXDeR1Olb/ZHrW489MJcXObx8WlVy+WM0vXK2q/0XCL+LS+S0nyomXL9XHUwMDAzd4zGkcMktFS8nmRRXHUwMDBiukNexi+WXHUwMDE2PeYx/43iqH/ajv9KdTStfppl+mBcZt+tM0hJVIJ2sdVcYsPLRqXSLidi6WXCqD5cdTAwMTapdXzrNG1cdTAwMDRZdZS8vmVj03dFJzFSp6znXHUwMDEzqsnXW66vJa8/ZGOqf4C1JetcdTAwMTJGO4VTemd+2MhcdTAwMDBVYJXnXHUwMDE0XHUwMDFjSC8hsmFrXHUwMDEwqo1cZri3XHUwMDA3elSWrHIsg8l1Q4pcdTAwMGL95tLyXHUwMDA0rDPqK0L1aFB7O4hPb1oywuWl53RcdTAwMWVqcpXCXHJK/pZcdTAwMDbJTU/S0mrF7W608vL3ZvuxQOpfXHUwMDFkhdBcdTAwMTdF6eh+37H6fetcdTAwMWRzg9mj9NHZzX796v60dt29yOS1PCnBdpjsKI3OkGrkQ1x1MDAxNLRA6e1o3SmRJ6GF8WSeXHUwMDE433huXHUwMDA29zG1rSmYXHRcdTAwMWK2JoRqkoOMdfi6XHUwMDFh/VdcdTAwMWP9ilx1MDAxNjFzTDg+Lv176SXiPiaBXHUwMDAyXGaN52PB30yR6aRrjFx1MDAxNPJcdTAwMWR1XHUwMDE3U+91XCJNXHUwMDE3hFxulCNpY6Xpn1x1MDAxYTmyOYyrZT2FXGLe4m7UlMqLT1uv8oHzvHvGcz9cdTAwMTIhJ3BcdTAwMDBSYVx1MDAwZcBcdHR82ICa0Fx1MDAwMseK/smyZi6rmGA1XHUwMDA1ky/gXHUwMDAwcZF+eihYiq5iUpwnXHTCfcOF9laPXHUwMDA3elBcdTAwMDFdI3fBrWelNm682OFr2Vx1MDAwN9FGQYzCsjDiqr5J3IN71HE9oHRa0vB/b+5cdTAwMTFcdTAwMGJg/kmNYfeLyIeRscVcdTAwMTc0XHUwMDBloKDrZ+9cdTAwMGaDXHUwMDA3qdZaeLjse2F2I1u73c6Bc8l2YOSZXHUwMDAydl7kw4zhxZAh/+UlXHUwMDA2SE5cXDllLZeifJ//slx1MDAxM1x1MDAxY9Yk9tE/1O1cdTAwMGKXQ6aRj0/sXHUwMDBlf5t8XHUwMDEwnfaRpv7fTT56g2DfS1x1MDAwMvlcdTAwMThcdTAwMWHPx8hcdTAwMDeXp8VcdTAwMTkvzbAw7zr+aPrNTqTxXHUwMDAy9yVBpygmeTJgXHUwMDFjyT2QTlx1MDAwYlx1MDAxMNGT/Vx1MDAxYe99fO1DibSHqHzCeJ1cdTAwMGZQsCZUxnKX7HFTNuTdjdJeKvLfWsFY41x1MDAwN7DAgfUri8a/xfhm5Fx1MDAxZdNDQSSGXHUwMDBiJTiuXHRyr9xBYFx1MDAwMlx1MDAxM+CVTor0XHUwMDFj4ZWy3HOByPHH2Mf0ziZcdTAwMDOyM2FcdTAwMTTc5Vx1MDAwMFx1MDAwNaBzgLxs/nuTjTjA8k9qXGarX8Q1tI7lXHUwMDFhUvSLe9/RwnzdbKz2uqc5dyGOb25qVX1cdTAwMWFmk801NK9cdTAwMDKR7XnBp11ZO9xcdTAwMWKaZjvgynVNXCKK7suUXHUwMDBl+5+lXHUwMDFhOKk63Y4lRHmbL1x1MDAxOdxcdTAwMTdcdTAwMWVcdTAwMGU8jWp8b8NcdTAwMThUVnxIaX2aaiz91//Wn5dcdTAwMWGmV1mZ4beZJ/uYNMRcdTAwMGZcdTAwMTJcdTAwMTLw8VvUKOKAtO85pWA6Jlx1MDAxMmni3lx1MDAwN55z4ZJcdTAwMWKB2JGaK1wiXCKBRd7VzkdcdTAwMTR4XHUwMDFiT0g+rSbIv5NIXHUwMDA3329RJGDSKqTl4lx1MDAxNW208dKSjejx7Shk/PRiO5+OfsinWOiP2OiMjGR6wFiKZlx1MDAxZZDPylx1MDAwND6uwFC8x/GdXHUwMDFmKuBSW3IqgMhV31x1MDAxZt2MMnVcdTAwMGL+0JCI8lx1MDAxMNEl5Vx1MDAwZp6X7u3YkFxcoPlwXGbNjVx1MDAwZpif/N51V6l4XHUwMDA09y+PgfeLKFxuQPwxcyRcInjh/Fx1MDAxZOdg1dxGXHUwMDBmM+kw18hgunS0JY5cbvvbiXZg4HSgNVx1MDAxZj/FvJDsYPR4XHUwMDAwXHUwMDEzOEt6SkpyXHUwMDBicko70lx1MDAxOc7BmurB7IQ2uZFcXPvPTlx1MDAxZlxc1+Hn2C/p06slkfa/QyxiXHUwMDEwKX6yiJVGvlVabjYn0oXI28yDLryO5dmo/nix21x1MDAxZvlm87hDs/Tq42j+a6WXrzp4x1x1MDAxZve18sPK5OV8XtH/48VQ2VwiynxcdTAwMWL++vuPv/9cdTAwMGZcYuGAVyJ9 virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp"},{"location":"guide/widgets/#region-updates","title":"Region updates","text":"

    The Line API makes it possible to refresh parts of a widget, as small as a single character. Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive.

    To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer. Here's the code:

    checker04.pyOutput checker04.py
    from __future__ import annotations\n\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Offset, Region, Size\nfrom textual.reactive import var\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\n\nfrom rich.segment import Segment\nfrom rich.style import Style\n\n\nclass CheckerBoard(ScrollView):\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n        \"checkerboard--cursor-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard > .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard > .checkerboard--black-square {\n        background: #004578;\n    }\n    CheckerBoard > .checkerboard--cursor-square {\n        background: darkred;\n    }\n    \"\"\"\n\n    cursor_square = var(Offset(0, 0))\n\n    def __init__(self, board_size: int) -> None:\n        super().__init__()\n        self.board_size = board_size\n        # Each square is 4 rows and 8 columns\n        self.virtual_size = Size(board_size * 8, board_size * 4)\n\n    def on_mouse_move(self, event: events.MouseMove) -> None:\n        \"\"\"Called when the user moves the mouse over the widget.\"\"\"\n        mouse_position = event.offset + self.scroll_offset\n        self.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4)\n\n    def watch_cursor_square(\n        self, previous_square: Offset, cursor_square: Offset\n    ) -> None:\n        \"\"\"Called when the cursor square changes.\"\"\"\n\n        def get_square_region(square_offset: Offset) -> Region:\n            \"\"\"Get region relative to widget from square coordinate.\"\"\"\n            x, y = square_offset\n            region = Region(x * 8, y * 4, 8, 4)\n            # Move the region in to the widgets frame of reference\n            region = region.translate(-self.scroll_offset)\n            return region\n\n        # Refresh the previous cursor square\n        self.refresh(get_square_region(previous_square))\n\n        # Refresh the new cursor square\n        self.refresh(get_square_region(cursor_square))\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        scroll_x, scroll_y = self.scroll_offset  # The current scroll position\n        y += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\n        row_index = y // 4  # four lines per row\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n        cursor = self.get_component_rich_style(\"checkerboard--cursor-square\")\n\n        if row_index >= self.board_size:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        def get_square_style(column: int, row: int) -> Style:\n            \"\"\"Get the cursor style at the given position on the checkerboard.\"\"\"\n            if self.cursor_square == Offset(column, row):\n                square_style = cursor\n            else:\n                square_style = black if (column + is_odd) % 2 else white\n            return square_style\n\n        segments = [\n            Segment(\" \" * 8, get_square_style(column, row_index))\n            for column in range(self.board_size)\n        ]\n        strip = Strip(segments, self.board_size * 8)\n        # Crop the strip so that is covers the visible area\n        strip = strip.crop(scroll_x, scroll_x + self.size.width)\n        return strip\n\n\nclass BoardApp(App):\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard(100)\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp \u2585\u2585 \u258b

    We've added a style to the checkerboard which is the color of the highlighted square, with a default of \"darkred\". We will need this when we come to render the highlighted square.

    We've also added a reactive variable called cursor_square which will hold the coordinate of the square underneath the mouse. Note that we have used var which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor.

    The on_mouse_move handler takes the mouse coordinates from the MouseMove object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down.

    • The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. We can perform this conversion by adding self.scroll_offset to event.offset.
    • Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.

    If the cursor square coordinate calculated in on_mouse_move changes, Textual will call watch_cursor_square with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. The get_square_region function calculates a Region object for each square and uses them as a positional argument in a call to refresh. Passing Region objects to refresh tells Textual to update only the cells underneath those regions, and not the entire widget.

    Note

    Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.

    The final step is to update the render_line method to use the cursor style when rendering the square underneath the mouse.

    You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red.

    "},{"location":"guide/widgets/#line-api-examples","title":"Line API examples","text":"

    The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration!

    • DataTable
    • RichLog
    • Tree
    "},{"location":"guide/widgets/#compound-widgets","title":"Compound widgets","text":"

    Widgets may be combined to create new widgets with additional features. Such widgets are known as compound widgets. The stopwatch in the tutorial is an example of a compound widget.

    A compound widget can be used like any other widget. The only thing that differs is that when you build a compound widget, you write a compose() method which yields child widgets, rather than implement a render or render_line method.

    The following is an example of a compound widget.

    compound01.pyOutput compound01.py
    from textual.app import App, ComposeResult\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\n\n\nclass InputWithLabel(Widget):\n    \"\"\"An input with a label.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    InputWithLabel {\n        layout: horizontal;\n        height: auto;\n    }\n    InputWithLabel Label {\n        padding: 1;\n        width: 12;\n        text-align: right;\n    }\n    InputWithLabel Input {\n        width: 1fr;\n    }\n    \"\"\"\n\n    def __init__(self, input_label: str) -> None:\n        self.input_label = input_label\n        super().__init__()\n\n    def compose(self) -> ComposeResult:  # (1)!\n        yield Label(self.input_label)\n        yield Input()\n\n\nclass CompoundApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    InputWithLabel {\n        width: 80%;\n        margin: 1;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield InputWithLabel(\"First Name\")\n        yield InputWithLabel(\"Last Name\")\n        yield InputWithLabel(\"Email\")\n\n\nif __name__ == \"__main__\":\n    app = CompoundApp()\n    app.run()\n
    1. The compose method makes this widget a compound widget.

    CompoundApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e First\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0Last\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0\u00a0\u00a0\u00a0\u00a0Email\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    The InputWithLabel class bundles an Input with a Label to create a new widget that displays a right-aligned label next to an input control. You can re-use this InputWithLabel class anywhere in a Textual app, including in other widgets.

    "},{"location":"guide/widgets/#coordinating-widgets","title":"Coordinating widgets","text":"

    Widgets rarely exist in isolation, and often need to communicate or exchange data with other parts of your app. This is not difficult to do, but there is a risk that widgets can become dependant on each other, making it impossible to reuse a widget without copying a lot of dependant code.

    In this section we will show how to design and build a fully-working app, while keeping widgets reusable.

    "},{"location":"guide/widgets/#designing-the-app","title":"Designing the app","text":"

    We are going to build a byte editor which allows you to enter a number in both decimal and binary. You could use this as a teaching aid for binary numbers.

    Here's a sketch of what the app should ultimately look like:

    Tip

    There are plenty of resources on the web, such as this excellent video from Khan Academy if you want to brush up on binary numbers.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaU/jyFx1MDAxNv3ev1x1MDAwMjFf3pMmnlpvVY309MTSQCCsYe2nXHUwMDExMomTmCTOYidcdTAwMDRG/d/frUBcdTAwMTNncchcdTAwMDKM0400PVx1MDAxMNtJueqcU+de36r8/WVtbT16bHrrf66te72CW/OLbfdh/Xf7etdrh34jwEOs/3fY6LRcdTAwMGL9MytR1Fxm//zjj7rbrnpRs+ZcdTAwMTY8p+uHXHUwMDFkt1x1MDAxNkadot9wXG6N+lx1MDAxZn7k1cP/2n+P3Lr3n2ajXozazuBDMl7Rj1x1MDAxYe3nz/JqXt1cdTAwMGKiXHUwMDEw3/1/+Pfa2t/9f2Ota3uFyFxyyjWvf0H/UKyBko++etRcYvqNpVx1MDAwNDQ1lHP6eoZcdTAwMWZu4+dFXlx1MDAxMVx1MDAwZpewzd7giH1pXHUwMDFkzppcdTAwMTdnwVOnSW865dxWZn9jc/dm8LElv1bLR4+1frNcbu1GXHUwMDE4ZipuVKhcZs5cYqN2o+pd+cWoYlsw8vrrtWFcdTAwMDN7YnBVu9EpV1x1MDAwMi9cZoeuaTTdglx1MDAxZj3amySvLz53xJ9rg1d6+Jek2lFEU05cdTAwMTRcdTAwMDHDxetBe7XQ4GggWjNcdTAwMTCEMdAjrdpq1HAwsFW/kf7PoF13bqFaxsZcdTAwMDXFwTmiXHUwMDAwXklcdTAwMGXOeXi5VymoQzhjnFHNKSHy9YyK55crkb01RVx1MDAxZInNUJJcdTAwMTEmqVx1MDAxMmzQXHUwMDEyrz8mXHUwMDE0NKGcXHUwMDExMlx1MDAxOFT7+c1ssY+Pv+I9XHUwMDE2XHUwMDE0X3os6NRqgybbXHUwMDAzX2OYXHUwMDFhXFzTaVx1MDAxNt3o5WOUUoZQrYlcdTAwMTn0Rs1cdTAwMGaqo29Xa1x1MDAxNKpcdTAwMDO09F/9/vtcdTAwMDIwpYQn41RcdTAwMTNcclx1MDAwMsDMjtM9dpJxm72oVT047Fx1MDAxYz1cdTAwMTGze8C/Lo5T9k44xWF/XHUwMDEzqMrBW+VcZlxiUcA4XGYjVVx1MDAxYYdcdTAwMDHiR1HBXHJ2i1pcdTAwMDaqUdtccsKm20YkTIKrcLBcdTAwMTlcXHBcIimlXHUwMDEz0MqBOJpcdTAwMTIjXHUwMDA0cI5cdTAwMDRjMIpWMJJIzpn8NLCaT1x1MDAwMWucmKNY5VpQI1x1MDAwNZiZsZrb2bvZolx1MDAwZlutWq8rRHGDkqfrIFx1MDAwMasjePsnUUqIXHUwMDAxhVomULFGQKpcdTAwMWSthcbxXHUwMDAw0Fxu4CNBylx1MDAxZEJBMi1cdTAwMTVVRulxlDLtXHUwMDAwXHUwMDEwjlx1MDAxYU9cdTAwMThwS6xRlCrNSJ9Sq4dSr1bzm+FEjIJcdTAwMTKJXHUwMDE41YIowbWSM2PU69ZPN77dXHUwMDFmXFzlstmb6kaudbW3XV5cdTAwMDSj7zXjz4BRXHUwMDFjeUMkXHUwMDEwoVx1MDAxMa7EsGGQXHUwMDAygpRLg8rGjIXOUnN+yZVMsnF8Uu5YRyFccuCEzpBcdTAwMTJiXHUwMDFjoJQ5XHUwMDEyQFx1MDAxOJRQwlAshVx1MDAxYVx1MDAwNShcdTAwMTeUMWpibfxcdTAwMTlcdTAwMDCqmE5cdTAwMDIo+jBcdTAwMWMxodTM+DSsV7nNRlx1MDAwN7nuUfd8v51vcdVopFx1MDAxY59SXCJcdTAwMDJcdTAwMDFcdTAwMDEqXHRcdTAwMDe0pSP4VFx1MDAwZVCBRGVUSFx1MDAwNVIsXHTQO3ScXHUwMDFmXHUwMDA1UIZyYlx1MDAxOFx1MDAwMbWCXHUwMDEz/TSE6uRpXlKMXCJcdTAwMDSfXHUwMDE5oHv5ulxmy2euOLmqXHUwMDFmh8F5buN6p5NygGqOYVx1MDAxMZM4W0itXHUwMDE4lyNcdTAwMDBcdTAwMDVcdTAwMDdRY7hmiFWtR83HfPik7E5r+Dh8XHUwMDEynFx1MDAwNriWn1x1MDAxNzV9jlx1MDAxMdUyXHUwMDExoFx1MDAxOERcbowk+excdTAwMTBccjJnV0FwuXta7mZ36MZGePh0t5duiCqG8ZA0SmBgLVx1MDAxMYxmXGKiUjJcdTAwMDSwZIDgXHUwMDExeFAuXHUwMDA10VKpNFx0n1x1MDAxM1x1MDAwMFx1MDAxOWPKj1x1MDAxOVxccpzCcVx1MDAxMp9cdTAwMDN/P5AwwFx1MDAwMn955XsyLF+vXHUwMDE5XFxcdTAwMWTDUuT1XHUwMDA2Jjo28s2Th92jTKV0obY297fJ6WbLO9Drr+d9/33y2z5f/PW47rmt/Xz1kGarXHUwMDE33lGzenFcdTAwMTJcdTAwMGV/yo/Pd9vtxkPsfV9+S+KSxrBSsHgotSSXhu4/RiNhXHUwMDEyaaRBgVx1MDAxYVxuYd+i0eTOnEyjiluodNpe+omk3o9IU1x1MDAwMzrGx+nExuiEXHUwMDAxNjZCXHUwMDEzmc6QbTDWjSDK+099S0uGXt1x637tcWi4+uDE/lkz8e5cdTAwMGI9/LxnJFx1MDAwZZ25UfPLXHUwMDE2uus1rzSM6cgvuLXXw3W/WIzPXHUwMDFkXHUwMDA1/HBcdTAwMTffsZ2dRfNcdTAwMWJtv+xcdTAwMDdu7XzQtsUnK5qciTZCXHUwMDEzYSPSmVnmV0/Oj2vlzMP+9l6tK+tHJrpkqWeZXHUwMDExyDLGNNpcdTAwMTFcZnHYsOVHmXE4xnnKXHUwMDE4jFx1MDAwYthcdTAwMDfm9lx1MDAwNHdsXHUwMDFhXHUwMDFjuKVcdTAwMTBIOSm7x5hcdTAwMDPKSGyyTeRQYsbzJngpXHUwMDFlg3nC0oUmtXR4LWqSXHUwMDEzKqibIONcdTAwMDH6W/DduoON6FwiMsXb++vTXFzwcHK2XHUwMDE1h9o/9Vx1MDAxY+VtXGJrKlx1MDAxZI6ux3CDs7PUZlx1MDAwNMLCMTgmXHUwMDE4XHUwMDFhUUYwMljKcVx0UiBSTZokXHUwMDFj++Rcbj9cdTAwMDQ7XWpKJ6CXOlJcdTAwMTJF7dRtjb9cdTAwMWVcdTAwMDUvN4JK8vGGLFx1MDAxZNhlsXFcdTAwMWRPWHOFgVx1MDAxM5394cp+b/eCXHUwMDA2N1t1cXpPj8J6JnNZ99OuvUZcdTAwMDFcblx1MDAxYdpw9DFCXHUwMDFiMVx1MDAwZVxcITHg11x1MDAwMiNdJZeKZj9Fe5F7XHUwMDEy47t/XHUwMDE2v/Fu/1D8isRAVzFcdTAwMTQhXHUwMDE5XHUwMDFmz7fgXHUwMDFifVx1MDAxNU2/c1jyXHUwMDAytl1RXHUwMDE5kTO79HZcdTAwMTW0XHUwMDE3J1x1MDAxOFx1MDAwN4HKudFcdTAwMTKNOGUjXHUwMDEwXHUwMDA2h0hcdTAwMDGaXHUwMDE5gtJrlkHwh0qvfZzLmFwiep5s4TtGw88oyOe/XHUwMDFkdO/Pe3dunlSOXHUwMDAyUz2JVP3dwlb0d7FHt1x1MDAxZkpccpmYRqdAUTw4mUPaN/d6XHUwMDA10T5Sgp1vfDvdr9Jet1dLu7RjbzuUY8dcdTAwMTNcdTAwMDFExaPCV1ttn1NcdTAwMTP0JahasFxmMT5B2pmy8bdcdTAwMTK/iLRzQVx1MDAxM/HLJEhOyVx1MDAxY88p71xcssd446RVydz1mt6J37qst1ZB2y2G0XegfFNcdTAwMTR3zSeEhuhaKMNAg/LlQsPfNGjPTEi2v4e4o61cdTAwMDai8YRfXHUwMDA0vclVS1x1MDAxOP9cdTAwMTjCXHUwMDE5mT1cdTAwMDHf2rlcdTAwMTf+dnBcdTAwMWJcXJU293dP+d7N8U1SIUhqxFx1MDAxNyiaXHUwMDEyXGb4lFbow1x1MDAwMMRcYnCFI21lndBcblx1MDAwNZGnPadhn2VRXHUwMDFlL877qeGLTE9cdTAwMTRfyYnSqFx1MDAwNLPj9+yqVbx42pG128JZVVx1MDAxNKNm7f6gsFxu4lx1MDAwYlQ7XHUwMDE0XHJcdTAwMWJojFx1MDAwZlxy3vN4bKhcdTAwMTDZwI1SiObUii9lXHUwMDFh8Dao+UXgy03yXHUwMDAzepuZxLBRzVx1MDAxZVx1MDAxNz5cXFb35bedyq6f9erB8VngPVx1MDAxY6Q+rYF65lx1MDAxMCptvVxmXGLsj9G0XHUwMDA2c9D14pQsXHUwMDE1pfFAIJ3ySzlGK4LJX1x1MDAwNcBCJeY1NKP2QVx1MDAwMZ1dfm9Ubkfz4lG1ZIr5Mjts1VtVs1x1MDAxMvKr7MNHNPpoXCK0UWN5XHLEsCRGM2LQbi1ZaPKh8quFXCJGxiOan1x1MDAxYr06Oatsi4Ml9sbsqYf7w1x1MDAxM9K4PLy99MONh43QXFxH+fOktFxcauRXceUwjNgwNiNcdTAwMDTFS41AlztSoCnGIaGGkbRnle1iIFxmt3+Z3IOEZP+A3Vx1MDAwNMJQmD33kMtcdTAwMTauvcr1TeusXrjNPmRcdTAwMGav+fHxKuivXHUwMDA1sTRacS2YfVx1MDAxNqTHQUxt9lx1MDAwMf1cdTAwMTSVii9cdTAwMDPij9VfyVx1MDAxOcM2/ir6K6eU+INcdTAwMTbGMJgjdVbee3wqXHUwMDExt37VaoS3hzdcdTAwMWI8W8Omp1xcf7VE4bOVSURb68BGrUNfXHUwMDE2gSphUcGXeibyXHT6y4kkiH4+T5nqXG7jXHUwMDE3ZGL2gffLis1cdTAwMWNld1ftzVMhzEGvfXfwKFx1MDAxYrVcdTAwMWJcdTAwMWFcdTAwMTb2V0F90Tw4VKBNsE/0uIxVwP2AMChbXHUwMDFiheBcdTAwMDK0walVX4bmXFxY+/CLgJcmi68xVGCwMrv3velcdTAwMTVOdffkXHUwMDAyrlx1MDAwZnfunlqd/cvk1Vx1MDAwManRXjSLqL3CYM8zNbR+91V7cUKWiFrN4s44ndqL5uHZ//xcIvCNXHUwMDE1q43AXHUwMDE3XHUwMDE1XHUwMDAwea7nSPxcdTAwMTa7VffxdrtcdTAwMDS3j3fu08XxiV9/zK6C9lwiT1x1MDAxZEJcdTAwMThcdTAwMDDKL1omOlx1MDAxZb4pZp8/UvtoWaU48Vx1MDAwYii9gPj+eZxvUq0+Jcm6S23yXGKHi8yM3MKW2cs8VkpcdTAwMTc7ep9lg9Pc0Vx1MDAwZZUrgVxcw1x1MDAxY3RcdTAwMGKEXG4lXHUwMDEwejCCXFxcdTAwMDKOXHUwMDEyNppcdTAwMDctUZ2TXHUwMDBi9lx1MDAxMTeuWFx1MDAxMLmxRN1cdTAwMDCqZFx1MDAxNJv9dYHA4lx1MDAwNYbLXHUwMDE2679cdTAwMDJ3Ql1P7mJcdTAwMGZuu7RLOlx1MDAwN/eiXFwq34WN2FQ6XHUwMDA0sbnrelx1MDAwNKNcdTAwMWFiOYPFllx1MDAwMsQqUN5YXG4wNC6DlVx1MDAwMGLoxFlXXHUwMDAyRI1m0jKAoVx1MDAxYlx1MDAxOK35J9NL/pN4XG7JZUmKISbto6qZaXr+UKrlNk0+XGLK7u5Fdvss4zG6XHUwMDEyNLXPXHUwMDBlKWFcdTAwMTjWac3VyMNxXCLt7MPxP1xmX5VKtkjLsHSSXHUwMDFiXHUwMDFhI6mdgDBcdTAwMTD5+PU0n09cIppcdTAwMDZcdTAwMTLRxUhERTKLXHUwMDA0XHUwMDAyRtrtNWZmUY5+bfaK1b2C2JK1m8JJt83yuVVgkVbELoanXHUwMDA2JzOiOZ1AI0BJsfGzkmy0XZ862VEmXHJX8bTxz8Mkllx1MDAwNiaxXHUwMDA1mVx1MDAwNIllhuhcdTAwMTjR/+NcdHpmJu2dbXSzl5fhrp/fyarr6Lq3ebGzXG5MwlDGYVLYXHUwMDFiRnuoKYwxSYKtz5bckPhjoHdl0qRcYmeMSZRcdTAwMTOpOPC5kqGrwiSRXHUwMDA2JolcdTAwMDWZlFxcs6BcZlx1MDAwN61cdTAwMDWfPf66XHUwMDBmz13aOFx1MDAwN5/Ip8fbsOh+O6+vRNZcdTAwMTZcZjhcdTAwMDLjL4M84dyIcVwiXHUwMDAxKENcdTAwMTkwW8r9z1x1MDAxMkmjs/uUzS8+n0gyXHJEkouau+RtXHUwMDA3XGLaXGKGnTk7k85uy3tQb3qV7Y2n3YjSXG5/MKvBJC5cdTAwMWRFtX28rFx1MDAxNWej1T9cdTAwMDRcdTAwMWPWL4RcIooyPeVcdTAwMTHexzOJc6FcdTAwMTVTn7BR3OdcdTAwMTNcdNJAJFiQSDzR22HQIKyhmWNKOi+b/MmFyLiFfWRJJLdcdTAwMWW71XBcdTAwMTWIJFx1MDAxNbo3YiTBXHUwMDE5WFx1MDAwYi1GyjhwSuKcUlx1MDAxMFx1MDAxY12VgOQyjqWIRGYhkt1yz65cbvhcdTAwMTlnJJVcdTAwMDZcIqlcdTAwMDWJZJL34Fx1MDAxNVx1MDAxYeNaXHUwMDEy31XyLVwi8XLQpHukWij1NiumSup1fytpOUuqiKQpdZRdUkw44lxcqdFcdTAwMTlJONTuzStcZmg2bUnLx6dcdTAwMWJcZlx1MDAxMcauqvlcdTAwMTmJxNNAJL4gkZLTdlx1MDAxMuyihDm2XG7ptIS/V2td3T1G3V3voFx1MDAwYse3Okqg0WLbslx1MDAxNd2w4r3zdlLaIVx1MDAwNqdcdTAwMTljjJB0JDrCOdlcdTAwMTFcdTAwMDKhi7GRYYpM2XyVgyhcdTAwMTXUdFxuTdyYTc6U9pbcSPGu+1a+XHUwMDFjWIGd0pZjZzZodqJ//Ttcclx1MDAxY/3RlKlMfe7QXHRUTa5BXHUwMDEz9lx1MDAwYlx1MDAwM0Do2esgplx1MDAwZnA6d1CU4IDk6Fx0cUJDYzi8fkhcYuZQTe3G2VxiMymSXHUwMDBi2Fx1MDAxNyYqY1x1MDAwZfpVQFx1MDAxOVDcME5iNVx1MDAxOINcdTAwMWQ+ibGb4jNb1Gp3XHUwMDAxZWPfi1x1MDAwMFx1MDAxYVBr9Fxc+/ksOlx1MDAxMVwi1WQ8mzJcdTAwMDfVwshtR5t+UPSD8vpQXHUwMDA1xsu3fGRnkPw+OVx1MDAwYlx1MDAxZNtK4lx1MDAxMG1QSXH0XHUwMDAwhUzSwY5Ltlx1MDAwYtymXHUwMDFkYUdcdKLQunGFWGaoxi9nXGaqPLyg+Hajpm9cdTAwMTVcdTAwMTdrVIZYzFx1MDAxOIyUXHUwMDE5XG68xMbJsUZRh1GluMBwXHUwMDA1zyGcwVijam5cdTAwMThtNdA0Rtj5J1xyP4hGO7nfm1x1MDAxYpbYXHUwMDE1z1x1MDAxZJNcdLyp+LFRXHUwMDA1aNp3XHUwMDFjluzBb2tcdTAwMDOW9P94/f2v3yeenVxmYvszXHUwMDBl38H7fYn/f26bkVx1MDAxOPditCdcdTAwMTiXc+z+ynVR+81vN6Ql/bPcXHUwMDE27+RPL5LyRynRLiPAkYaAfC4vieVl7PVcdTAwMWGYY5fzckSixDebkjlaVLtgtlx1MDAwNKyiknE6X1n34i4jXVx1MDAxYtss5zLyXHUwMDBmPkaE6bBcdTAwMTmvbVnMZ0Dy5pdGg/0yhtlr3adcdTAwMGZxOrmqwG4wK5GLz1x1MDAxYt5cdTAwMGZzVWpHKk5sZa4ynCfv0bMwVzGmt3swo1ZcdTAwMTiceDSZQF1cdTAwMDNcdTAwMGVcdTAwMTdcdTAwMTLDfi2pxKhg/NuXXHUwMDA04OVcdTAwMTBflPuRNmNhrs1oM6ZL/vCMjlx1MDAwZVx1MDAwMnWMK+A4hDqW9Xid0rmDc5/hKLtcbm9cdTAwMDAnw8VsxvTd2WKNsjWxXFzbXHUwMDFkXHUwMDE57JdcZlHB2ViTKDpcdTAwMTFA8ae2JsNcdTAwMTiQK20yXHUwMDEyXHUwMDExbH8yY+B9L4+RuMaXXHQjQZHZM1x1MDAxObXC3XaVX3tX8p7C42XuW/787C7dskWtdzOGXHUwMDExm1BcdTAwMTdGjqyNXHUwMDA0YVVNc1uCi1xu9/6qJSdsLz8xXHKo3u/LYaaZi4+srkVlofEt8j/aXFzk3Duvllx1MDAwZW/xoylcdTAwMGJaiymlTVx1MDAwNsNcdTAwMTJcdTAwMTLfjuvNXCLBqSOcVpIyW9XE7ZbwXHUwMDA02Ti8f1x1MDAwZqC5QFVcdTAwMTRMKrvNLUwphl+UpoY7dr9jw6xcIlxuPcFbyH5cdTAwMGLt14xxobiKnfLDW6CEXGJit8H4XHUwMDE0b7Ew1Wb0XHUwMDE207V+LZ7CwFHhxm6/ZPc+l3w8WWB3PVWKgTJcdTAwMDKHj6nFjMX0pVx1MDAxZSNuXHUwMDA3m4KjiZG60XbhsFx1MDAxZW+UcLTRXHUwMDAynaxcdTAwMTREYYipV9pbZJJcdTAwMTBsf8awm2Qtvry8/7rbbOYjRNzrcCCW/eKLQlx1MDAwZm5yvet7XHUwMDBmm5NJZnn25aU7rep4/SVL3798/z/RhT5iIn0= 901245673Input()Switch()Label()

    There are three types of built-in widget in the sketch, namely (Input, Label, and Switch). Rather than manage these as a single collection of widgets, we can arrange them into logical groups with compound widgets. This will make our app easier to work with.

    Try in Textual-web

    "},{"location":"guide/widgets/#identifying-components","title":"Identifying components","text":"

    We will divide this UI into three compound widgets:

    1. BitSwitch for a switch with a numeric label.
    2. ByteInput which contains 8 BitSwitch widgets.
    3. ByteEditor which contains a ByteInput and an Input to show the decimal value.

    This is not the only way we could implement our design with compound widgets. So why these three widgets? As a rule of thumb, a widget should handle one piece of data, which is why we have an independent widget for a bit, a byte, and the decimal value.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaVNcdTAwMWJLsv3uX+HwfJlcdTAwMTdx6VtZlbXdiFx1MDAxN1x1MDAxMyDAYEBcYiPWXHUwMDE3XHUwMDEzjpbUWkBcdTAwMWLqXHUwMDE2ICb831+WzKDW0kJcdTAwMGLybVx1MDAwMSYw9KKurjqZeTIrK+s/nz5//lx1MDAxMvXawZe/Pn9cdFx1MDAxZYt+vVbq+Fx1MDAwZl/+cMfvg05YazXpXHUwMDE07/9cdTAwMWS2up1i/8pqXHUwMDE0tcO//vyz4Xdug6hd94uBd19cdTAwMGK7fj2MuqVayyu2XHUwMDFhf9aioFx1MDAxMf7L/cz6jeB/261GKep4g4dsXHUwMDA0pVrU6vx6VlBcdTAwMGZcdTAwMWFBM1xu6dP/j/7+/Pk//Z+x1nWCYuQ3K/Wgf0P/VKyBio9cdTAwMWXNtpr9xlx1MDAwMkMtwGqAlytq4TY9L1xuSnS6TG1cdTAwMGVcdTAwMDZn3KEvT9XLm28n33vfOrlCrprJt2VcdTAwMTFLg8eWa/X6adSr95tV7LTCcKPqR8Xq4Iow6rRug4taKaq6XHUwMDE2jFx1MDAxY3+5N2xRT1xm7uq0upVqM1xiw6F7Wm2/WIt67iXZy8FfXHUwMDFk8dfnwZFH+ktZjzPNJVx1MDAwN8tBoVQvZ93tdNBDg6BcdTAwMTQ3hiGzI83KtOo0XHUwMDFh1Kx/sP7XoGFcdTAwMDW/eFuh1jVLg2uwqIKyXHUwMDFjXFzz8PyyXHUwMDEywWOCc8HBXGJgTL5cXFFccmqVauRcdTAwMWGiwZPGcC0541x1MDAxMjRcdTAwMGXGLlxm+oNcdTAwMDKSziM1cXC3e357v9RcdTAwMDfIv+Nd1iw9d1mzW69cdTAwMGaa7E7sxEA1uKfbLvm/xlx1MDAxZZTWRiDn2sKgXHL1WvN29OPqreLtXHUwMDAwLv2jP/9YXHUwMDAwp1x1MDAwNEZMXHUwMDA0KpeaXHUwMDAzXHUwMDA3XHUwMDEwM1x1MDAwM/VwL7NxcJcrlc+yqnfUqu9njoVcXFx1MDAxY6j8jYBKw/5cdTAwMWFStWeVRNRouZExXHUwMDAw9G9X6Fx1MDAxOSOBsGxBW7RmXHUwMDE5pEZcdTAwMWS/XHUwMDE5tv1cdTAwMGVcdTAwMDFhXHUwMDEyWtFTglx1MDAwYlx1MDAxNExcdTAwMDLAXHUwMDA0sFxuxTxcdTAwMDPMXCIqISRcdTAwMTiuxsDKjdBKk5Z5Z2DVXHUwMDEyXHUwMDEysVxuKNFqasrMWL3LXHUwMDE2dq5cdTAwMThuPcHm4VZWl0s3XHUwMDE3359cdTAwMTKwOoK3v1x1MDAxMaWSXHUwMDE5bYVS1oBRYyils6TMNKCyTOtcdTAwMTWiVHhcZpQkUaGnWW3GYcqNp1x1MDAxNFx1MDAxM2hcdTAwMTTjSlx1MDAxMKTHYGolkKDhOqrUoF6vtcOJXHUwMDE4VUYkYdRcdTAwMDJaxpTUM0P01lx1MDAwNPzkoFx1MDAwMPmbrd7RiVx0ermjy5NFIPpWXHUwMDE2/3WIaushXHUwMDExXHUwMDFjXHUwMDFhc6GsiGnK/u1cdTAwMWE9lFx1MDAxNmjotbJWxPpqXHUwMDExk1/2JXGLcXiCoDZwLq1cInvOkSz3XHUwMDA0m889qVx1MDAxNFpSoYxcdTAwMGLBUY/iXHUwMDEzJFx1MDAxMVx1MDAwMuBkXHUwMDAy31x1MDAxNUC10ElcdTAwMDBcdTAwMDVcdTAwMDKv5ZbPXHUwMDBl0KenzUaNXHUwMDFkXHUwMDFlbaszWVx1MDAwNX9v92t19yzdXHUwMDAwXHUwMDA1pjwynMqANcYy4GJcdTAwMDSiwuNkPi0zilx1MDAwYkviuiREXHUwMDBiRDlXXHUwMDA1UU28lIGAd4ZQm2jmOelcdTAwMTR6aTE7QkWb1W52oqPsTvXmQF1cdTAwMWbnXHUwMDFizcJWylx1MDAxMSrQXHUwMDEzXG40k2RnLfkvOIJQwlx1MDAwNYGT/Fx1MDAxOYtkXcUo/5hcdTAwMGahwFx1MDAwYsaoVSGUXHUwMDEzOrVUxM7WXHUwMDBmoq84TixcdKRgiXxLLYWcXHUwMDE5pfXNzon6epCv6G+t81x1MDAwNopM/qmeSzdKqas9TV6xXHUwMDAy8kaI5ckhkHJpyLcnT8pcdTAwMWHpPEi1XHUwMDE0Rsvl8iSATkBkrM//a8aJf1x1MDAxMvNcItdtXHUwMDBlXHUwMDA0/lx1MDAxN1x1MDAwYlx1MDAwMzSI5yM/k4H5cs/g7lx1MDAxOJqi4HHApGNDn+/ulvf38o9cdTAwMGbly1x1MDAxM7v1vWefXHUwMDBluzdfXq77+fzbb0P9UDtjgJc2XHTvhlx1MDAwNpuTzzl7mGDyO0+Ge9UvVrudIPWAV+LtXHUwMDAwP9X5ilx1MDAxMZaBtzVcdTAwMGV74thcdTAwMTaRpzRcYjBcdTAwMTjrVjM6rT31jTtcdTAwMWI6uus3avXe0HD1sUn989nGuy9cZuh5fcVrhq7crNcqzb52XHLKw5COakW//nK6USuV4kq+SFx1MDAwZvfpXHUwMDEzO/uz6OZWp1apNf16ftC2JazKlLCxZuQsky2dnfucibNN0auD3GiI7c7DztNcdTAwMDO/a6ZdzFx1MDAwNFwiiVx1MDAxOSlsziwoXHUwMDE0w6E4XHUwMDE0xrMo+yFcdTAwMDUgeK8uXHUwMDEyh8LTTFklXGaToKScXHUwMDE0i+PcU9pKbUnyXHJcdTAwMDNmx2NxKFx1MDAxNbXXmnnEcCHrk1x1MDAwZVrEWWL4XHUwMDAzhJRcdTAwMDaQ3P6ZXHUwMDAxfHC2KcOL6Nthpn2OXHUwMDA15V/dV6r3f/+8x1xmIFbSM8BcdTAwMTVcdTAwMTB7N9rYYVuByDyJZCVcYlrWWFx1MDAwMtdS7FxiWZFJPclQeIzcXFxcdTAwMTIoJS11PUxcdTAwMDAweORBanJ6ibxcdTAwMGIjzSh+OWlcdTAwMWOpXHUwMDE5zuNhrjV8XHUwMDEzWVx1MDAwZVjqSnLI2OzwbTX09fZ2ttjL3NbuNkVcdTAwMThcXFx1MDAxZUo/7fpXcu2Ri0lcdTAwMWVcdTAwMWYpNOByXHUwMDFjuoygXHUwMDA0XHUwMDFjOCNcdTAwMDeUL1x1MDAxNcH7XHUwMDFkXG6YMcmN4UZ/XHUwMDEwXHUwMDA0y+T5PMWJunJtZvdLg+2Tk30ondejnVx1MDAxZvLxurXFi9dmXHUwMDFkXHUwMDE0sETjcSFQgVx1MDAxMJprMCMoXHUwMDA2wo22StF1xDFSq37pXGZcdTAwMTNcdTAwMTaIQ3xcdTAwMTD0qsTgtJFSxVxy6WvQLdT3KmdcdTAwMDeb96fFQGm1n+ncdjN3aVe+XHUwMDAwzCNqa1x1MDAxOTjSKKRcdTAwMWVcdTAwMGVOo7BcdTAwMWVjKDRKXHUwMDAxXHUwMDFh5OqczDdSvmQwrTBcXHxcdTAwMTD4XG6ZXHUwMDE4XHUwMDE0JFx1MDAxMWdO+9qZXHUwMDAxvHt4e3pZzp0/tJqN/ePK9SPb7lx1MDAxZa+D7nUgNlx1MDAwNFFitprQg6PK17lwmoNVTFxuS65cdTAwMWMug+J/XHUwMDE4ZVx1MDAwMjshfv1cdTAwMTbql8yHm1x1MDAwMjRcdTAwMWaE/VxuTI4+SO2yo+RcdTAwMWNJa2fnu5eH29vlXHUwMDAzfVx1MDAxNlRcIlZu4o/N9CtgRbzBcMVcdTAwMTjXqK1cdTAwMWVGLpFfXHUwMDEwXHUwMDFjmJFutmOFXHRcdTAwMTZvo35dhlx1MDAxMCNcdTAwMDb/QbQvxnyVUfKgLLWC4eza965Z7+BBuVuA4kE2f3GQffpRTprZTpf2VS7zhli+1eSxas3GMcystYJ4XHUwMDA0MSrUy8VcdTAwMWVWqX1dviZIXHUwMDBlXHUwMDFmxHNDlpzdpvv5fHaOXHUwMDE5xd5pMTRFznd2s51K5nLv5OvGblLKcGq0L+fWI8IghZtHUVxc4Fxidok5SKZcdTAwMTDRzX2DhKWYw+/gv1x1MDAwMP08zY+igE1y7EGgy5+Nx9deQzDkq6adLVx1MDAxN24u9+5cdTAwMWHb19nKg9pPQdL7bCgmssQ5aWF0UbIxXHUwMDEwI1x1MDAxZFdSo1Nvy2VvrFL/KlwiOvTNPlxi+0WTXHUwMDFj+yW/jaivkLOjt3rQyYeNw/bOWXi3u5FcdTAwMGZt8VtcdTAwMTZTr3+N9lxiXHUwMDEw5LZcdFx1MDAxN26B8ehcdTAwMDO4XHUwMDE4XGZotDruLKRU+yrSOZqA9EHUr9TJXHUwMDA0wjDUXHUwMDEwp1Kv4fe+yO5OK7WwsbeFm9cnd5FfbZXXQvtcdTAwMWFDXHUwMDA0V1x1MDAxYmktM1x1MDAxNuOZXHUwMDE4LyBcdTAwMTY0NuS/uUVYfLnMpJXyX+E+wX6U2K9MzpyHflx1MDAxN7mMlpnxe+37aifb/da5rCt21oyAIT9Ku/5Fhlx1MDAxZVfoXCKmjjnwUeZgPcFcdNZklC1PfeqDXHUwMDEyaDR5mlx1MDAxZlx1MDAwNL5KJUZcdTAwMWaASa5cdTAwMTXOXHUwMDEzfrjZle3vR+qSseuef3WV2z82lfN1UL9cdTAwMGXC5KGRLTJWS8PNuPrlVmtcIsdcdTAwMDC4ZHL9SpWvZkyTz/1RtK/iidqXM6Vccqp4mspr8O12evuHXHUwMDE13Fx1MDAxMVet4F5vZXgzb3qp176qr32ZtVx1MDAxY7hcdTAwMTAxr/1F+2olhVx1MDAxNkwxxLRcdTAwMDd/wdL7WMnYXHUwMDA3XHSfUZcl6l9LbJBjXFzdvFx1MDAwNuDHvdtKeHPh8+vHXFw1v3lzXHUwMDFmXT9l10L/9leBgjbKXG5Ffc/lOIpcdTAwMTGFW+5O/2C5OYzVTr4xg1x1MDAwMqx8P5mTSan1XHUwMDAwidDlXGbBXHUwMDE5ojnm3W7bX4PM1VnQ/V45yD49Ni/CcmEt5o0lck9cdTAwMGLnsVx1MDAxMWqJXHUwMDAxj0RcdTAwMWaAk/7lXGKSVLNEmMJcdTAwMWRcdTAwMDRcblx1MDAxZlx1MDAxN0RubLRcdTAwMDdQZaPYXHUwMDA06Vx1MDAwMtBcdTAwMTg3mqvMrid/NVx1MDAwNpHFsutjXHUwMDFm8Ep2/VDPXHKS63HowlmT66NWOymzfuhcdTAwMDVG0+jZ9Cz6JFHSyYtbXHUwMDA10VFrxFx1MDAxY9lvW6J9b3PhUfugoDG30WlcdTAwMWSZncw6SFx1MDAxMlx1MDAxYfQss8LNYVx1MDAwYsVwNInTZVx1MDAxOXEpXWcoMlx1MDAxMlOWYS8jSpNoy7gkUWPcYtx5WMq6XGJcdTAwMTKkQZBgMUFcdTAwMDKZKEnkyoJi8VmtV1NB7ptf4ewqd1x1MDAwMUenbV3M5i+KrcZaXGJcdTAwMTKxXHUwMDE0cNEnRiBlio+LkbHOPLtcXDzF/1aLhIKcXHUwMDEyXHUwMDFiX4L7flx1MDAwNImnQZD4goKkk1x1MDAxN1xuK7DkW1x1MDAwM5+d3PXC6sPj7db53fX33Yvz5tXF1d7xWqSlXGJcdTAwMDaelsI4v1x1MDAwNInLjYpcdTAwMTLJmZGMPG9cdTAwMTJcdTAwMDNjkr2SpSRpklx1MDAxYjIuSeSSXHUwMDE4l1x1MDAxZP5cdTAwMWW5XHUwMDFkpkGScEFJSk4vUORcdTAwMGVYXHUwMDA1c1QuMVdcdTAwMDf4taDOiz/OXHUwMDFh55nocKdcXM111kGQOCrPOs+USVdcdTAwMDVCglx1MDAxYZMkblx1MDAwMazi2lx1MDAxNV5YlZs0kyhx5Vx1MDAxMu90LMfs/UiSTIMkyUXJnUmSJFJ+Uiszz1wiocvDo4ctc7b740T29i5yZYjqtbVcdTAwMTAlsjVcdTAwMWVcdTAwMTlcdTAwMWNcdTAwMDZE4MhcdTAwMWRcdTAwMDJtxkTJoGJcdTAwMDY12Wlr2GrcpFx1MDAxOa1cdTAwMTJcdTAwMTlPRoZxrnDYusiSSoMsqVx1MDAwNWVcdJP5XHUwMDFk00R7mJhjyX6mlM1sh+a8cZMrXHUwMDA03ZNWbvPworJcdTAwMTayxJXHXFzNLGt5v0TTqCwxz0pE1NZVfF2VKLFcdTAwMTlFXHRcdTAwMTmq91x1MDAxObzTaVx1MDAxMCW9mChxlrz+RLmiySDZ7GZcdH50wzB6aLDj8z14ZFmf5UpJ1S9SJUpCcY9cdTAwMTO3XHUwMDAzMJqTXHUwMDBlMSPRO2Y90iuuQFx1MDAxZFx00rRVKKuPOmhUXHUwMDFhLYjfXHUwMDEzdEAyw/DbJEmkQZLEXHUwMDEyxWRscja2XHUwMDE0ynKNZnbD1Ny/vTu4lFulzrm82snrS773mH/jXHUwMDE5/ZJcdTAwMWZWg1x1MDAwNFFiXHUwMDBiiZJcdTAwMDTjITdMMlx1MDAxN1x1MDAwZZdsJOrAhMcsXG6ppNJkqKfU4Sha7nN/qihNndN3XHUwMDE1pN1cdTAwMDNcdTAwMTQ3rmrNpNJlWnmayKawQjBqT9xxe/amXHUwMDE4J1x1MDAwZahQvZ3dej4xUrLsOdRkbOPh4TJ7XFyyt5uV3Yub3o5ccl/ebFxihn6n03pIYc0ySF6PS+4zMVx1MDAwMFx1MDAwMDV7XHUwMDE0+yravc0yntssfisp2/leOdkpJk1cdTAwMDelR1x1MDAwNJRcdTAwMTSeXHUwMDA1REt9bp1cdTAwMWIzJFx1MDAwMsJcdTAwMTJt45qTOpBcdTAwMTIlT15Rs6xcdTAwMDRALD9jSt0yJVG4+mlvZ1HWXHUwMDA345Ps1exF0bZq0elDjVx1MDAxOMw//2ey5VqsOtqilivenKnC+6tjJ0hvPFx1MDAwM2tsa1x1MDAwMtMvkzBHStr0oU5pSppC6TF62X6VXHUwMDEylzI5Yr9cXD14QSBcdTAwMTP0rfiUelx1MDAxMEvbL+Zxi1x1MDAxMo0w9CBcdTAwMTNPm1x1MDAxZlxis/Ikama50UowXHUwMDFlr/k6qMVJhpjFd5RIZVHCMPI70VatWao1K1+Gsoqet4XZn8Eg9CW22O1rbk+7jG3JXHUwMDE5aMuJdsjYRVx1MDAxNb9Nl1x1MDAxOI+4LVxyN/F+RaOtn89cdTAwMGbSloJm6fVcdTAwMTZNZ2lDLVx1MDAwMueZ0oOk1Vx1MDAxY21sqeVLk8Ajb11Zolx1MDAxY9KQbVx1MDAwMDueS1X3wyjTajRqXHUwMDEx9XyuVWtGoz3c78pNJ+LVwFx1MDAxZlNcdTAwMWH0UvFzo7qg7T5xWIlcdTAwMGZ++zyQlf5cdTAwMWYvv//7j4lXbyRj2H2No3fwgZ/i/y9W2FFPKSymjVuKPUf91Ivi9cHTRifs7FfK/KZ519RVm5RcdTAwMWSeXHUwMDFlJmKkJ1x1MDAxNaGNSctcdTAwMTSPr2v5xUS0p6VcdTAwMDRgVkvNpuSkXHUwMDAwKUO/sLguI5XpKdRu6p5+XGJpJySqXHUwMDE4XCLj5NhKrV1cdTAwMDaeVLFuet69QjO0Kp4/uUqeclx1MDAxNvpPcHxRr1x1MDAwNyivRKF1nrHifM24uJ1qzSWNylx1MDAxY8vbj/Ld7t2PTDPfXHQvXHUwMDFl7cVcdTAwMTM8QEGnXlx1MDAwMiR6iEBcdTAwMDZbWaflcZiLS4PkXHUwMDAxXHUwMDEy+pnb1YdjMlx1MDAxN19WXHUwMDAyJm4sNKGGsHSRUivecCZ8fUC+JFx1MDAxOe9FwX6z3Y1SQsZjzVmMjHNIXFzexFx00W5XojnSWKZcdTAwMGZ1Ssk4MOOJX8XXmDDkTVx1MDAwZlenkFp5XHUwMDE2hVx1MDAwMSO1ZdM2XHRbVnyJqlCPc86MsYbHp/hfhNlcdTAwMTJ5c3tHWGO4dcHkMdnWxEJcdTAwMTU19H1Q8en2IEZ8N4j5up3C0FxyoUFr9ThcdTAwMTfXnuHSXG43TSU1qlx1MDAxNyY4J1x1MDAxOZ/O0obdXHUwMDAz0vi/Qn9cdTAwMTJVrNrugIuvNfVOXHUwMDA0rPvaXHUwMDE4w+qbXHUwMDEyb56Y6kBcdTAwMWTNNeBcdTAwMWMxhLMrv36+fdPbPtBfe49cdTAwMGa7ufvd3lsvKp7GOlx1MDAxNtySS3jo0qxcdFmCSzWyuSFnrqKv1lx1MDAwMiXTgvh3Mu2QRYustFx1MDAwNPFcdTAwMTbcI34vgLqd+t5MmGJcdTAwMTJgPCW0KzFM9IjJ8Z04ibZcdTAwMGJt4uuaVslJXG7lw9ZtXHUwMDA3mtunnfLpcclcdTAwMDS3N42t9Vwi3skxcCBcdTAwMTJKqnCeXHUwMDE4+OZjq7jTuS40N+Do/vvtzs2VLrdTL1x1MDAwMZxxr79lXHUwMDAwMuVKQlxme56ESE/2tylSbqNcIm2SXHUwMDBim/CC8UWwXHUwMDA08eaTplUnXHUwMDEwb7fRXHUwMDE1zLVpwHvB+PK8e6e/jXN6iPdLe1x1MDAxNmPekFxck95tXHUwMDFlIIycI+11+linlHiTufBcdTAwMTi3RJtcdTAwMTVze9uO7NiHzPFcdTAwMDdcdTAwMGJCMUcweHJCxNLia0hRKCPIXFxcdTAwMTJNIco2gXlL44JLxlx1MDAwNbpcdTAwMTUqsmJcdTAwMTOkW1omh2pcdTAwMDGvM/Weblx1MDAxMoapN2Ouklx1MDAxNtFu5crzgIs7j1Nd8MiddDXPXHUwMDA1uVOAOFx1MDAxZXaeiX5PJ2tD9Jsxji40LJjWNGh2kFx1MDAxZftOXGJ4MnDd1zhkk1x1MDAxOPin5yd88dvt04iA9tL/XHUwMDA04VrpWV1cdTAwMGZe88t9LXjYmryjm9vU7dNzhzrFXHUwMDEz9Fx1MDAxN3D//PTz/1x1MDAwMcOHY8gifQ== 901245673BitSwitch()ByteInput()ByteEditor()

    In the following code we will implement the three widgets. There will be no functionality yet, but it should look like our design.

    byte01.pyOutput byte01.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258abyte\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the compose() methods of each of the widgets.

    • The BitSwitch yields a Label which displays the bit number, and a Switch control for that bit. The default CSS for BitSwitch aligns its children vertically, and sets the label's text-align to center.

    • The ByteInput yields 8 BitSwitch widgets and arranges them horizontally. It also adds a focus-within style in its CSS to draw an accent border when any of the switches are focused.

    • The ByteEditor yields a ByteInput and an Input control. The default CSS stacks the two controls on top of each other to divide the screen into two parts.

    With these three widgets, the DOM for our app will look like this:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1daVPjSNL+3r+CYL/0Row1WXfVREy8wdnc0OZo4O1ccsLYXHUwMDAyXHUwMDBijG1kmWtj/vtmXHUwMDE5XHUwMDFhSdZh2fiiPUPTLclyuiqfzCezslL//bKwsFx1MDAxODy33cW/XHUwMDE2XHUwMDE23adqpeHV/Mrj4lx1MDAxZvb4g+t3vFZcdTAwMTNP0d6/O62uX+1dWVx1MDAwZoJ2568//7yr+Ldu0G5Uqq7z4HW6lUYn6Na8llNt3f3pXHUwMDA17l3n/+yfe5U79+92665cdTAwMTb4TvghJbfmXHUwMDA1Lf/1s9yGe+c2g1x1MDAwZd79//HfXHUwMDBiXHUwMDBi/+39XHUwMDE5kc53q0Gled1we2/onVxuXHUwMDA1VET0XHUwMDFm3Ws1e8JSXHUwMDBlVFx1MDAxOcPDXHUwMDBivM4qflxc4Nbw7Fx1MDAxNYrshmfsocUr9nK7d9N9rJtm6eB43Vx1MDAwZs5L5+vhp155jcZh8Nx4XHUwMDFkiUq13vUjMnVcdTAwMDK/dev+8GpB/dfARY6/v6/TwkFcYt/lt7rX9abbsd+fvFx1MDAxZm21K1UveLbHXHUwMDAw3o++XHUwMDBlwl9cdTAwMGLhkSd7hWFcdTAwMGVQQY0xRIDR6v1s7/1cblx1MDAxY6GZltQoKpRhfXKttFx1MDAxYThcdTAwMTMo17/EXHUwMDE1q1V5KNllpXp7jeI1a+/XXHUwMDA0fqXZaVd8nK/wuse3b0xEONB117uuXHUwMDA3eFCa8PPc3rhcdTAwMTOliFx1MDAwNk01fz9jP6W9WeupwH+iI9OsvY1Ms9tohILZXHUwMDEza/1qXHUwMDEzVZ2Y+lx1MDAwNO5TKG1ksluVdndcdTAwMWJMUN7erizz48btaaVZXny/7p8/0m/7+ubO3vdye31cdTAwMWb2dlx1MDAxZb7V+MWe/H7sPsU/5dfnV3y/9Vj0vjv+wVx1MDAwZn61U/7O+ePekeeXvNXvlWL3fftbOIDddq3yqutEKlxyXFyDUSZcdTAwMDKXhte87Vx1MDAxZttGq3pcdTAwMWLC40tE4Fx1MDAwNCxj41x1MDAxYUGkzEakXHUwMDAwbrSRQlx1MDAxNkZk+iSNhEgyNURSMI6IIDJcdTAwMDKC3lxyXGadXHUwMDE2JClNQpKKXHUwMDA0JKlihlx1MDAxMEXk2CCZoYVKXHUwMDBiqUBLoYbQwnC2W83g0Hvp2XaIXHUwMDFkXa/ceY3n2IT11Fx1MDAxM1x1MDAwN2j5OXDXev7m67+jI9lx8ZPtrYiOvWep4V1bNV6s4ndx/ZiGXHUwMDA3XHUwMDFlurD3XHUwMDBi7rxaLeqUqihIXHUwMDA17+lvXHUwMDE2cSYt37v2mpXGUZqcucB7XHUwMDA1flxu8lxiRIal31x1MDAxOVx1MDAxMqI1IyxyxSDs5du4ecWe0Fx1MDAwZYBUgilKXHUwMDE1NSBj2KNcXDqCXHUwMDE4oVx1MDAwNbUjolQ2+KD3XHUwMDFhXHUwMDFkfEY5QDgxXHUwMDA0lOT4k0SiXHUwMDEwXHUwMDBl2lx1MDAwN4WAXHUwMDEwXHUwMDFhgCnSXHUwMDBmTHtKgpHDuMrQqfxSXHUwMDE4+nbknynDtVx1MDAxM1T8YNlr1rzmdVxcsDfOV1x1MDAwNCf2O1fa1q04hlx0nFPKcUg0ZyxyxVWr2rXfo1x1MDAwNFx1MDAwZcd5XHUwMDE30nAj0c1IymTiy7vN2mChLq7u/aP9l2Zru37hi43qQ1N8f0xcbmVcdTAwMWOCdp5cdTAwMWHG0KArSXm6UIJcdTAwMDHHadRIzigorlx1MDAxMjI1Kp1gpXV351x1MDAwNTj2XHUwMDA3La9cdTAwMTn0j3FvMJcs8utupdZ/XHUwMDE2v1P0XFy/iWjbO8apUvi3hVx1MDAxMEG9f7z//T9/pF5dylJs+0qodHi3L9HfWbYtl+tTKmX/4Xf7XHUwMDA2SC6oXHUwMDA2XHUwMDE1erxB9i1/jmdi39Qg86aII1x1MDAwNdVUcEMojm+c7DNcdTAwMDKOMlx1MDAwMq1cdTAwMWLHWUDjJ/rkXHUwMDFhn3UjOiQ171x1MDAwNi1cIv4vXHUwMDBihryTcsZDXHUwMDFmO1x1MDAxYrJ/6W8/updcdTAwMWKb9eP6snfmnrfXl1fOZk32L7rPOytwt/tcdTAwMWPcd8pAXHUwMDFloHJTL81zXHUwMDEwMZI/XHUwMDE4KYigVOssqGtmJEFcdTAwMTdQnMmkz/6cI13mI11MXHLpTCWRLpNIXHUwMDA3gaKCoWND+qRjiEi4MyCGWPnF7L/+bNpcdTAwMTNV9NdcdTAwMWS38/fPxaDV/rn4s5lcdTAwMWVZXGJcdTAwMWW703vg0HCvgtHjilx1MDAwMW6rP64oIPtcdTAwMDc8slxmeVg/TFx0Rr+AlEdcdTAwMTfH6crhnqqdbHzb89ae2lxcb7i+u+TNe/6NXHUwMDEx9MlcdTAwMThYIf9AXHUwMDAyQkGyXHUwMDE4UjlnjqaMIDUhXHUwMDFjgOpMoH442jeQXHUwMDA0qkpE+5Io4JJFvtdsXFzyzfpccl8+u1/deKhcXH6/a+12N48vr4q6uJuLk12x3azed4+f6jdUXHUwMDFk3d22XHUwMDFlxuA6XHUwMDBmbsr153OvfHrbeVhT61stf3XDXHUwMDFiw323r6ube+tcdTAwMGZPasv9UTnaa+/drWP8MS6XzFx1MDAxNGOhXk3KJUtNM7HOuOGaUiiO9fTpn6lPLoB1XHS5WEd6zlwiWFdcdTAwMTPDuklL7CV8sjXBlKE846Pf8+OUbcJss9nuXHUwMDA2WXm9XGbv+9G83lx1MDAwMCeVltf7JebojpYh581cdTAwMDKfMMC1UkNk9nboXHUwMDBm/2bHXdl9WuvSMrku7Z+Wm/PuZylTXHUwMDBlXHUwMDE4xJbgXHUwMDFh/zPxxJ5cdTAwMDTiXGKNJopcdTAwMTjFpFx1MDAxMlx1MDAxM3SzJMXNykTujjD0sdJwOevQl2JQeVjdO395emr5vPHy/HJ/dTRrP3tx841cdTAwMWVtPK2xTX/l4rl+u+5cdTAwMDdwPob78v3DXHUwMDA3s7NcdTAwMTI86lx1MDAxMu8snWnXK2k1Lj+rqWZcIlT4XHT5WcZ19pI2hnpcbqO+SEZ4XHUwMDEw1tOnf879LOU8XHUwMDBm65Q6MFx1MDAxNayblKx90s1SQimyXHUwMDAxXHUwMDBl41vSXHUwMDFlp1x1MDAxNn7QzXrB4aNcdTAwMTdU619hun52gJNK+NmInFx1MDAwYlx1MDAxZlx0aVx1MDAxNWR6WoywMJzT0lx1MDAxNF/AfvK8yr1/e3dSWq+wXHUwMDE3ft899PZmvIgmXHUwMDA3pp7AXHUwMDExxiiOP4wzwXhcZn2cI8lVlFx1MDAwMVd4XHRcdTAwMDOSzXI/nHoyPIk/naS5XHUwMDA06ZEydIy5p9FcXO3a/snN7qM8XT2g66XT1WD7eUOuTSFcdTAwMWI82HVNKWurc7CjqUKFkUOkg9KHc86xI3KxI+LY6WfP41xcnyFJ6CSztkhmmcJIVY4vXHUwMDFiND9cdTAwMTHia9j1mvbslSrWW42a6//9c/FcdTAwMTKDss60XHUwMDEzt1x1MDAwM1xcQb9DKyR9LkwzXHUwMDBiRVx1MDAxNCGZMLWL2ZrpyIrdwHXUXFzDNadcZpNI5lx1MDAxMFsqXHUwMDAwXHUwMDA2tZ/RXHUwMDE4TJm2oSaA0JZeXG5cdTAwMWVZdlx1MDAxZTdOwaFcdTAwMTRxITV+XHUwMDE0YsPoXHUwMDE0jyeYXHUwMDAzQjFcdTAwMDCNYT5cdTAwMThcdTAwMTFBwHuih4PNtqthKrg+XaVIwaJcZulQw7mixtbgXHRJZFpVXHUwMDA2OFx1MDAxOEpJW4kqUG6NLMYkvnyhSpF8VL9cdTAwMGJFcJ654UJcdTAwMTBbmWKkyKhfXHUwMDAxblBcdTAwMWKkoTjjSlFJXHUwMDEyUn2qWpFs9bavpGKHN/xcdTAwMTL9PbSFo1xcZK5cdTAwMWZcdTAwMTODNIUoPVx1MDAwNIvPT4PMqYljWjvSXHUwMDE45F2ao1wi0ThcdTAwMTVBaDiK4yBQsGUz0ULtcVx1MDAxN8JRXHUwMDA3yZ8gqM5MUqHCiVx0iVx0d4iU1GhGgWiTXFy00ohcdTAwMGIzY/uGcpFIiD9++5afWY7bXHRcdTAwMDaaIZxcdTAwMTjYKmI0/ilW0NixJoxwUFx1MDAxY936aOYtP1xuj8uEQ8RcYrJeXCJ6dc9JmexKqVwi1iRTYVx1MDAxZFx1MDAxNv/cxi1Tse0rodJDmrb8JFx1MDAwNc+u9MVcdTAwMTkgXHUwMDE0lVx1MDAxNYrnXGJ3L1a+lcs3zD+puZetnSXxtNahc15cdTAwMWbDwDjoVDRDXHUwMDAzJ1x1MDAwNbKPOIMj3DGMgFx1MDAxMpzgXHUwMDBmzsLkXCKtVINcdTAwMTZJirwlXHRcdTAwMTFcdTAwMTNcdTAwMWMjv1kvXHUwMDA3bJ2s3DWvK8es/qw3ZGX95P6YXHUwMDA2s15cdTAwMWX/XHUwMDFkKtY4z2RcdTAwMWNI5jgyXHUwMDEyXHUwMDE4XCL3kT5Nc1x1MDAwZUkh8iDJtMOnXHUwMDAzSV0obY9cdTAwMDabXHUwMDAzj/Ki3yf1XHUwMDExVn1NN2k/wJNkXHUwMDE3p4285yW6kbBcdTAwMGZ2XHUwMDFj41x1MDAwZWLZbWHU5du3OeX5XHUwMDFjtFx1MDAwM5pcYs21pHh5nOczbVx1MDAxYyEw1jWogISTSEZy/LlcZoZ2TuGAI8pcdOEqJVx1MDAwNckxOqfCbv/UWuNcdTAwMTU6uVx1MDAxYk1cdTAwMGKD0IzuZfpccnNcdTAwMTn5QInzalx1MDAwMkJyYFx1MDAwMlVcdTAwMTlcdTAwMDNpmuTVysGJ14ZzrnrUO5k0KMT1XHUwMDBix1x1MDAxZuBgyI7ScMlQ+YxkJpbOeFx1MDAxNVxuldIuliE1XHUwMDA2IGj4PzfXz9Zt+0pq9ZBsP9u+oYPKsm+Ca/u/LJ6qzedZc2rfcMxcdTAwMWSltVwiXHUwMDFjObTk0X3mr1v6LOtAdkVcdTAwMDBpXHUwMDA3V5NcXFSh1JGglOQ2KWzT5CnMn6HeS6KYZFRcdTAwMTiI7C59s29cXKJcdTAwMWVcdTAwMTk6VLHApzNvhTf1UVx1MDAwMTiQhNg9myotJ1xuXHUwMDBlx1x1MDAxM1xubZvhQFxig1x1MDAxMXf0XHI0uL8kslx1MDAxYslcdTAwMDBQjVx1MDAxOLHqlCqSoIg80au8xVA8ZZPhZ7Jt2VptX1x0fVx1MDAxZdKy5Zc1yki7ikSaXHUwMDE2Z5zhNFx1MDAxNN/Rx5fXuydrm62V04p7UX95MK2no/05XHUwMDBmmohgjpBMqN6ym2AsXHUwMDFlNSlGXHUwMDFjXHUwMDA13O5lRf+OqjY541asrlEjhVRy9u076lx1MDAwZlx1MDAxYvewXq67bOX2qnbaofWDy4PfvfwwXHUwMDFhNE+q/FBFdnb2QVx1MDAxMqmcXHUwMDEyisjiXHUwMDAxVfoszTlcIqXMRaSgXHUwMDBlnVxuXCLT1oKTaVxmLa2QZjo1/kPrYDjVo6QxdiqXbuPrz0X4ufjvhakmMlx1MDAwNriS/kRGXFzQXHUwMDBmOEStMkszKJJcdTAwMWZi2DBcdTAwMWLqXHUwMDBlzrfcs+f7Yyit7m6elPeuyrf1kzmHXHUwMDFm1cg4rLZJXHRUXHRcdTAwMTJcdTAwMDdcdTAwMWa6Q0OQiFCMXHUwMDAzlJi5N1Q4I2Cj4Fx1MDAxOXvD/bVcdTAwMDefL61cdTAwMWZcdTAwMWRstq+6Tb3JO+V9PVx1MDAxZl7LkGhcdTAwMTOWSXktozKz78hsXHUwMDE1V2irXHUwMDBiwyZ9NOdcdTAwMWI2XGYsj8yCXHL6LD5cctjIlK5cdTAwMTApLsvG8kzy6dTLXHUwMDBmq4BcdTAwMWZzWW9F6FN2V1x1MDAwM1xmfb+7XG6FzEVbdlbKiEw3hbGxJU5miEYsuWx8XrNSXG5pIJpcdTAwMTZGXHUwMDE1XHUwMDExOtqAoVcnrLjDNZ5lXHUwMDAwljH3yzU+xFx06ShJXGIyM2X7f6mUjaE2O8mRI1wiU1x1MDAwNIPxNEvm3Fx1MDAxNcG36qhlmEF9jaJ6tI0tXHUwMDA1k1LD1LIwu5hiXHUwMDA3lnDbtimSXHUwMDEyXHRrWVxmgM1cXDGMXHUwMDA2XHUwMDAwVT7x5Vx1MDAwYuWl8olmXFwornBcdTAwMTJccqBMVEguaVIocIxdf+VS4JByopNCfabMVClTue0rqdbh/b5Ef49QPZhTXsOBMsX0XHUwMDEwW93z+dW82jejUd9Q4yjYoSe0b1x1MDAwZp7CIJlcdTAwMTnCJVx1MDAxOE3BTM7AcVxmXHUwMDA3mN1Qj26FoUFlKVEx5460+1x1MDAwNFx1MDAxNcFYWEqW4OnUNiuj3JjZXHUwMDE2XHUwMDEwXCJcdTAwMTOZZNa9sIGzffKQMFx1MDAxYZtXt/tyeKxz3ZstYY5cdTAwMTFcYitbISGYXHUwMDFk99FcZlxcPjWJXHQlXGJcdTAwMTFcdTAwMDS0XaCgXGIxkpRcdFx1MDAxY2XLqI0t39WA4n1q+5at2r2z/Uo9pHlcdTAwMWKQeYfsblx1MDAxZb2F5KF6hZ7cXHUwMDFmXFysLYlD+dgodVx1MDAwZk42Lk7rL4ejmbjpdVx1MDAxNEDn4dhcdTAwMGVcdTAwMTmcXHUwMDBiTanS8ZhcdFx1MDAwN8hRXGZcdTAwMDRcdTAwMDBnyI50dtA0rY5cdTAwMDJcdTAwMTKhoW2XlbGFTe9cdTAwMWE1VLKhc7ZVW/Jqa+Z4/9ErLZeO5OXZ9lx1MDAxNDrh5N634ZLzXHUwMDFk0Tl9MFtcdTAwMDcvXHUwMDE1v96+WSndjeG+3aPmcX1rp7y0XHUwMDEx1O9fSof+j63zsW3L1EyISMg8qeSINJlduoSxLbmH6VxunDr5c05muGF5SKfMoVNBetp+5pS2PcCR3JNxds1cdTAwMWOnXHUwMDBlhnP9sX5cdTAwMDJquqWJXHUwMDAzXFxUdj9cdTAwMDE1cpqEyuwulkTbsihFSPE8Sb7pnFfoMeIwRjFcdTAwMWVlxvbcVvE4QlDuYEBLMNzoXHUwMDE5oslVJ2JcdTAwMDTg2MpcdTAwMGWjgCkqIa2Fllx1MDAwMlx1MDAwNykos5V0XHUwMDFhf6KlRG9xXHUwMDA0Tlx1MDAxYTBOh+rqM/Y4QlxiSkfKaI57I1x1MDAxMjhUXCJZx+jbprq0MskwQjjEXHUwMDFlN4RcdG2f/ZBk7IWiiHz0xqNcYklt/Vx1MDAwZVx1MDAxN7ZcZl0xlVx1MDAxMEk7XHUwMDEyY1x1MDAxZmVcdTAwMWK5aUWJTM7GZ1xuXCKy9dq+XHUwMDEyXHUwMDFhPdYgQqvMVkVWJ1RsZ/Ug81Zb3VIrRy+Px7v3z3urm9v+9f7jjDtcdTAwMTVcclxcdeFUOVxmXGZD48VRoYyMW7de9Vx1MDAwZdo1MDaLRcRcdTAwMDStW7FcdTAwMTiCXG6NTidatDCbXHUwMDEwgtzfXHUwMDFjlLdWa+X25Vn1uHPaZJuXT7831Udzwya+XHUwMDBiXHTNWnZBXHUwMDFkKG2fVjBE5jJ9muZcdTAwMWOSnDg6XHUwMDA3krZ8ZyqQTOvAkkL2kVx1MDAxYtk1hOl0YFx1MDAxOVpcdTAwMGI/RvZ/lcWoqdfvXGZwJln1O+qj9TsmO9TWUvPeWlFh+Fx1MDAxZEO9Jsjyj4fjhzOoPnWPbneuunNcdTAwMGU/ZF9cdTAwMGUyfeRV1G5cdTAwMDeH/vpcdTAwMWS7tIaUXHUwMDEx9dDk9Medkju0JVx1MDAxMchcdTAwMTWVnnVKbf3+XHUwMDA2bpmskcf79o/d5TX329Nz4c5hXHUwMDEz9Vu2gymfuN+yj8fJ9FtcdTAwMTTokHVv6cM558DR1NHZwFx1MDAxMeDoKVx1MDAwMKdYXHUwMDAxXHUwMDBmwUBdXHUwMDE5JulUMlRDq+DHnNZsKnhcdTAwMDbY+vFX8GSXeWtkioxcdTAwMDEp3lx1MDAxZiefkc9pZsp2wLG8XHUwMDBiXHUwMDE0aIK0MN5cdTAwMDNMKoaAY1x1MDAxYc+B4Vx1MDAxYbKbzH54gdtcdTAwMTJSYXu54MdQalJcdTAwMTDIpW0mQoxcIr0nN5DEU1W17fIuh2s2Pe60lEGNXHUwMDE56jFcdTAwMDSRXHUwMDExLZSWKpxcdTAwMDOybaeEwbhcXFx1MDAwMtJdXCIjnfhcdTAwMTdcIkvJtm8oKIlRkaIjPykun2jGheJ2kLTtXGbJKfL+lDV34iiGsVx0Z0Joxpn55Fx1MDAxZHIyVdu+XHUwMDEySlx1MDAxZN7uS/T38Gl3XHUwMDE2cZ79xcD24YHUXGaxZzafXc2rbVx1MDAxM9ox9slrRCvOXHUwMDE19Fx1MDAxN+9oRzImeoVcdTAwMWaCkcl14kBcdTAwMDVcdTAwMDD7uC6tKUFcdTAwMWIlU9a/mHGQ/YHtQsqZ3Vx1MDAxY9tv26Tdi4k2epZcdTAwMWJmR6cg47ZtNn6SYG2IsrUhXHUwMDEwZmBDK8JcdTAwMWRj60okXHUwMDA2o4xSKZJcdTAwMGacLGTa8ilJXFwmxlx0t9V46E2FTjFsXHUwMDE4XHUwMDE0okhUo1x1MDAwMUT+ysTnzrlnqrV9JVx1MDAxNHpIu5ZcdTAwMTUjXHSZmWy3T3lcdTAwMTmi39fz877ZbW3uXpOT/c7SycqZ9+1ged5NXHUwMDFhU8ThvTJPaqjdnFx1MDAxZDdpgCaNgCUhhGnOJ/dQbZPymHuRiJC4ffpcdTAwMTmHKVx1MDAwNUh2q/pIzGukR2o7jvOz+ZUsXHUwMDA0db+7INNX8SNcdTAwMTMwXFyYXHUwMDE0tNpZMVLsXHUwMDBi9Vx1MDAwN0T9Qr1C7MtcdTAwMWKEXHUwMDE3K+32YYCj9m7ucD682ttXXHUwMDBmb7z44LmPy0mN+NdV72Xv2oOtxYjbczT/fPnnf+6uXHUwMDA3ViJ9 ByteEditor()Container( classes=\"top\")ByteInput()BitSwitch(0)Input( placeholder=\"bytes\")Container()Label(\"0\") Switch() BitSwitch(7)Label(\"7\") Switch() ...(1 thru 6)

    Now that we have the design in place, we can implement the behavior.

    "},{"location":"guide/widgets/#data-flow","title":"Data flow","text":"

    We want to ensure that our widgets are re-usable, which we can do by following the guideline of \"attributes down, messages up\". This means that a widget can update a child by setting its attributes or calling its methods, but widgets should only ever send messages to their parent (or other ancestors).

    Info

    This pattern of only setting attributes in one direction and using messages for the opposite direction is known as uni-directional data flow.

    In practice, this means that to update a child widget you get a reference to it and use it like any other Python object. Here's an example of an action that updates a child widget:

    def action_set_true(self):\n    self.query_one(Switch).value = 1\n

    If a child needs to update a parent, it should send a message with post_message.

    Here's an example of posting message:

    def on_click(self):\n    self.post_message(MyWidget.Change(active=True))\n

    Note that attributes down and messages up means that you can't modify widgets on the same level directly. If you want to modify a sibling, you will need to send a message to the parent, and the parent would make the changes.

    The following diagram illustrates this concept:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbU/byFx1MDAxNv7eX4HYL3uljTtvZ14qXV1cdTAwMTHabmm7QFx1MDAwYrS3vVpVJjHEJYmzjlx1MDAwM6Wr/vd9xkDsxHFcYmkoUF1LpYk9XHUwMDFlXHUwMDFmz5znec45M/n70draenY+iNafrK1HX1phN26n4dn6b/78aZRcdTAwMGXjpI9LXCL/PkxGaStv2cmywfDJ48e9MD2JskE3bEXBaTxcdTAwMWOF3WE2asdJ0Ep6j+Ms6lxy/+P/boe96N+DpNfO0qB4SCNqx1mSXjwr6ka9qJ9cctH7//B9be3v/G/JujRqZWH/uFx1MDAxYuU35Jdcblx1MDAwM6W2bPr0dtLPreWGXHUwMDE5LomsXHUwMDFht4iHT/HALGrj8lx1MDAxMYyOiiv+1Hp7I1x1MDAxOVxmXlx1MDAxZf4uP71qXHUwMDFldk5cdTAwMWJCfNz5XFw89yjudvey8+7FWIStzigtWTXM0uQkelx1MDAxZrezztXQlc6P71x1MDAxYiZcdTAwMTiG4q40XHUwMDE5XHUwMDFkd/rR0I9cdTAwMDBcdTAwMWafTVx1MDAwNmErzs79OVa84MUwPFkrznzBN6VYwPFHSSdcdTAwMWOXjunxZd9cdTAwMDHOXHUwMDA2TFx1MDAxMmdCa+Gc4VOGbSZdTFx1MDAwNlxm+4WOZLulXG7TXHUwMDBlw9bJMezrt8dtsjTsXHUwMDBmXHUwMDA3YYopK9qdXb4yJ1x1MDAxYmjmpOKKlDFE41x1MDAxNp0oPu5kaGJN4Egz6yRcdO6kdoUxUT4rVlx1MDAxYiVcdTAwMWOZYvi8XHUwMDA1g6127iF/loet375cdTAwMWO2/qjbLYz2XHUwMDE3nk17VdmzJrwri75cdTAwMTRvUvKEdCdcdTAwMWWdb1Bz1Njdf7v59Vlj820nXlx1MDAxZrf79tvsbi9ubkafs99Pn37gpn9+4I6V2Op3pp5y9fwwTZOzRftcdTAwMWQ4+vzcbDe/dj+yNOp8lVx1MDAwM2lcdTAwMGZW0O+zt9m2ftE8dPZsZ1s8c3uvKVmFve+fvZJvXHUwMDA3XHUwMDFmtYuy5qe9/us/WKPXWazfy0/FhI9cdTAwMDbt8Fx1MDAwMrhcXFx1MDAxYsuEs0qRLlxcvVx1MDAxYvdPpn2hm7ROXG6sPypcdTAwMTlcXGGZXHQ/KFx1MDAxM4w1NH16TDDWOSGVYHZhgpntVktcdTAwMTHMNI5vkWBcZlx1MDAwYpSyhitNmoQuXtffr7hccpzj3Fx0xom70mCsml9cXNH1mFCo8IBLXHUwMDA2IW1cdTAwMTiXJTO+l0BW6YPFTCf9bC/+6lx1MDAwN1vIwFx1MDAxOKOc0lJcdTAwMTitmJto9Dzsxd3zibnLfVx1MDAxNYO1m4/Tr/8qj+gwglx1MDAxMb5XZSfab3TjY+/P6y3cXHUwMDEypVx1MDAxM66exZDmcYPDJMuSXtGgXHUwMDA1I0L0mW4tXCKRSVx1MDAxYVx1MDAxZsf9sLs/beNcXPTN1XjFLK+FoDRcbjOxOFx1MDAwMF80P785y87Tk8M/PvRenGyop6/l2d1cdTAwMDLQXFyHP2FUoJ0w0mjOlZN8UuDJWK//Vlx1MDAwYoDTcuFqXHUwMDAxyPLjblx1MDAwNVx1MDAxZfZJRCpOr1xmn8tcdPxr2j7/LyUv9ejAfvncO2Bb7tPxzyrwbz5svd9I3iE0fNVP4iMxXHUwMDE4ZsasSIitI0xp4di3JMSIXG51XHUwMDFkXHUwMDBiQJtcdTAwMDRBf1x1MDAxNmaB2ZN/v1lAQmeVM2BDKa21JZnwt2tGXHUwMDAxlFx1MDAxY3PBXHUwMDE4Mabp1kiglEDMUWEr4Gyal7Kz25NheKDmdyfDm5242/7BKnyNjE2r8JWJ3yHC4PNaXHUwMDExhi44y1x1MDAxMFx1MDAwMS5cZkDqf+2evmxcdTAwMWOIvZPPO/uHZ+HWwba551x1MDAwMNRcdTAwMDJcdTAwMTCDe8DhhFx1MDAxNFx1MDAxNVx1MDAxMTZwXHUwMDFmidRVO01GqlvD30pEWCHFdkqy1cFzOVx1MDAxNY7C072NdP9Mmv2/tt8kfGh3mns/QNV+wnSYXHUwMDE0XHUwMDE38tZVXHUwMDE42VUtXHIwqaWU7lx1MDAwNvW22dN/z2lA+3zXh+NETDNpJnhAM1x1MDAxNVx1MDAxOMRDllx1MDAxOKJcdTAwMTI9p9r2Y3TYOKRcdTAwMDZCKLUyoK/SXHUwMDA3XHUwMDFmvFx1MDAwZV+jY0vp8Fx1MDAwNfZngE9oUa/BllxmyF6oxVPh+UnMfS12W1x1MDAxNzhcco+2UivLidRcdTAwMDT8oGtcdTAwMTBH4sxoq1x1MDAxOLPThlx1MDAxNfDjVqvw8DuKUSpAXHUwMDFjYJhcdTAwMDLtcTzRVNFoRIDQXHUwMDE00TozTlx1MDAxMKeKXGZjvlx1MDAxMFCQpZtUu1x1MDAwYmG58lx1MDAxOHF55ttyoF26hjXMwjRrxv123D+eNOxyVWeRilGO69bIW9lggVx1MDAxMFx1MDAxY1x1MDAxY8KJWbJWXCKyLzU7XHUwMDBlXHUwMDA3efhcdTAwMTNoLlx1MDAxZIfN1nKjuKy8fdRvX2/V/FxietIqXHUwMDEwkZPge8hcdTAwMWLhqFx1MDAxYeVcdTAwMDKtlOKwyFx1MDAxMUlesahcdTAwMWJcdTAwMGWzzaTXizNcZv1uXHUwMDEy97PpIc7HcsMjv1x1MDAxM4Xt6at4o/K1aYpcdTAwMTj4XHUwMDFlJ6Ow4tNagaD8y/jzn7/NbN2QNmCOw3G5VVx1MDAwZTGk1OX7XHUwMDE1hoOUVYj3cZVLc21/tUjxR1x1MDAwNSNFd4/K/9+YLJWoXHJUyFxi6Vx1MDAwNWbxOGV+pLlcdTAwMWGqbCfeOVx1MDAxNudKupYqSVx1MDAwNcz4ckH+skpPZiyai0AppoEgXHUwMDAxPiUzZdgqM1x1MDAxNlxuYIxcdTAwMTKet5mRZlx1MDAwNlfCXHJcZlx1MDAxN4asYX6lUlbL+iSVsfpuqXLp+GZBqrxcdClcdJJcZrNmpDbMKWZLsLpkJcFcdTAwMDLIoCGlreRcdTAwMDaR6XJMOT/GKVx1MDAxOcWCXHUwMDFj4dxIJpnmXG6onknfXHUwMDFhyavQxLWDUfSgybLetf1RcepcdTAwMWKSW1x1MDAxZbzO4DZuarnNK1x1MDAxMIaWXHUwMDE3VYHruK3RODxcdTAwMTl0e413nz703n462uc7NnyzXHUwMDFjt01cdTAwMTc9bi9cZoRPXHUwMDA3ym92IORcYkK5qWJcZiSHXHSGLFxmLlxi4qintpZcdTAwMTOhXGKXpzZJXHUwMDAxQWl8XHUwMDFjKiGYppSAjalccnpqXHUwMDEwqXCutJLWQVx1MDAwMitxoECw6inmzrhccjKAaEtcdTAwMTdcdTAwMTO4PLdNY7HmyopRPnFtxfFQ7Vx1MDAxY/ujOrsrXHUwMDAyuWD1q57EuGWalVxue9eh/JXZ33p3tN//a3evXHUwMDExd3Y/7vY2yzi+pyjnXGahKHNcdTAwMWF6XCJcdTAwMWSjyVJcdTAwMGI5XHUwMDFl+HqTXHUwMDE2mlx05Ma3XHUwMDA3cul11GJCuJRCilx1MDAxOeFcdTAwMGLoXGJcdTAwMTk4poWk1UaUmlxcVVxckUEohqt3inFokSpM+z/Gr47aXHUwMDE59kdlbm9cYvD6XHUwMDE0xcnps+NcdTAwMTRFwFuM0IunKPOXju9pNUdqXHUwMDFiKM9kklx1MDAxY1x1MDAxM0pNXHUwMDE2c0grjDyzSmryy1x1MDAxOfVrKpE25nsyXHUwMDE0K1x1MDAwMzyFM1x1MDAwYiVcdTAwMTZOmFx1MDAxOWsqXHUwMDA2TSxcdTAwMTfqosTNbaWY44zz+UkxXHUwMDEwP2Ep5yb5XHT32039tjHpk49S4lbkXHUwMDAy0pfymN9GZ1x1MDAwNMPs28rbL5Sg3KjAhEArz4c1hEXzqk0skPA0MLVcdTAwMGaJyIxcdTAwMGJcdTAwMTJcdTAwMGYzP/HFdMF93VxuaVwiXHUwMDFj1JXvbihcdTAwMTko5vNIvKt23OrruqvFSd5dXHUwMDA1XCKrXCJKcHLtJkxcdTAwMTCHVCRcdTAwMTYve89fhbunREkqr0JcdTAwMTJcdTAwMTG3xu+PniBK5D+B4yRcdTAwMTTmXHUwMDA1woHxqGXK7y17I71Hwmu19lm8sULPqnv7QoBcdTAwMTVcdTAwMDC+c1x1MDAwNKtLw3RcdTAwMTlcZjGEyoIxW9j5XHUwMDEzXHUwMDE2c1x1MDAxNqYlsJIzoCUlXHL+Icx1pVxm44qXMMPgLtKCIVx1MDAxM1x1MDAxMXrJqvdccmo5/udcdTAwMDJKYKqM407MsIhcdTAwMDXgUWNcdTAwMTCngDyMIFMx6SExpVx1MDAxNFx1MDAwMahcdTAwMTDqIJj0xepcdGqTKlBcdTAwMDZcdTAwMTFcYsf0cKmEvK63epj4o1x1MDAwMpBVXHUwMDExJWZrzs9hpDQgS7Z4TDl/I8Q9pUptKPDcXCKl31x1MDAxZlOiwjykJFx1MDAxYvhanZXI11x1MDAxOVx1MDAxOODWQkpHgfNLQlpcdTAwMTE8h5XixTFPalx1MDAwM6+CjVx1MDAxMpE+VNNUat7cec1G9Fvc/Fx1MDAxM/LkTerLXG40o1x1MDAwNVx1MDAwNtRxSZzPiCldQIjsuEDCRuAtW2Wl1caULIBNiLBcdTAwMWPYm/zCs6pcdTAwMTbiPVU643dDglx1MDAxMrhGXvOgqbIhXHUwMDFj0mXn02XwpCPi5dtcdTAwMWKKw/MtoCfhv1baa8myUYuV/GpcdTAwMDUmN2TLur1Mtv6HPbBcdTAwMDQxp6XC7a+jyk7nTTM92n16MNJnh9REYrT//PzeUyVcdTAwMDZcdTAwMWUyZIB0+G15xfTqh4O+oCm130RcYlx1MDAwN741ruRyxi97RKVMzpGpSFx1MDAwZeYuLLm1zUxcYscs5Hupgnh5M1x1MDAxM5s4O2f3Ulx1MDAwZuaGx9Fw7dfRYPZcdTAwMWUmXrOHqVx1MDAxYlx1MDAxZE369+RcdTAwMGWmLFx1MDAxOdRtX5p4mem9SpNcdTAwMDYthTBobFx1MDAxZMSQYlvos1i8gp18jJ/2qdGmdCS2XHUwMDA2zVx1MDAxZGaH2+9cdTAwMWVcdTAwMDLCXHUwMDA09EprST66nMrbiFx1MDAwN2SsX1xi9Vx1MDAwYqeO32LeZtRCXHUwMDEw035DkrP6x0DMr1UuVY9eXG5iYZal8eEo8z7dTs7691x1MDAwMmZVoy6g9uhSLtfDwWAvw7iNg1x1MDAxNcxI3L58+aLr9dM4OmtWveKXo/zwvebw9UCJ8jjx26Nv/1x1MDAwMC2LavoifQ== Parent()Child()Child()messages (up)attributes (down)"},{"location":"guide/widgets/#messages-up","title":"Messages up","text":"

    Let's extend the ByteEditor so that clicking any of the 8 BitSwitch widgets updates the decimal value. To do this we will add a custom message to BitSwitch that we catch in the ByteEditor.

    byte02.pyOutput byte02.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    class BitChanged(Message):\n        \"\"\"Sent when the 'bit' changes.\"\"\"\n\n        def __init__(self, bit: int, value: bool) -> None:\n            super().__init__()\n            self.bit = bit\n            self.value = value\n\n    value = reactive(0)  # (1)!\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n    def on_switch_changed(self, event: Switch.Changed) -> None:  # (2)!\n        \"\"\"When the switch changes, notify the parent via a message.\"\"\"\n        event.stop()  # (3)!\n        self.value = event.value  # (4)!\n        self.post_message(self.BitChanged(self.bit, event.value))\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n    def on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n        \"\"\"When a switch changes, update the value.\"\"\"\n        value = 0\n        for switch in self.query(BitSwitch):\n            value |= switch.value << switch.bit\n        self.query_one(Input).value = str(value)\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n
    1. This will store the value of the \"bit\".
    2. This is sent by the builtin Switch widgets, when it changes state.
    3. Stop the event, because we don't want it to go to the parent.
    4. Store the new value of the \"bit\".

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a32\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2503\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2503 \u2503\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u2503 \u2503\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    • The BitSwitch widget now has an on_switch_changed method which will handle a Switch.Changed message, sent when the user clicks a switch. We use this to store the new value of the bit, and sent a new custom message, BitSwitch.BitChanged.
    • The ByteEditor widget handles the BitSwitch.Changed message by calculating the decimal value and setting it on the input.

    The following is a (simplified) DOM diagram to show how the new message is processed:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPiSNL+7l9BeL/MXHUwMDFiMdbUlXVMxMaGb7vb99l4e8MhQLZlZKBBPjfmv29cdTAwMTZ2I4FcdTAwMGVcdTAwMDTGgHteOtrYkiilqvLJfDKrsvjvQqm0XHUwMDE4Pre8xT9Li95T1Vxy/FrbfVxc/N1cdTAwMWV/8NpcdTAwMWS/2cBTrPt3p3nfrnavvFx0w1bnzz/+uHPbdS9sXHUwMDA1btVzXHUwMDFl/M69XHUwMDFidML7mt90qs27P/zQu+v8y/7cc++8f7aad7Ww7UQ3WfJqfthsv97LXHUwMDBivDuvXHUwMDExdrD1f+PfpdJ/uz9j0rW9aug2rlx1MDAwM6/7ge6pmIBcdTAwMWH44OG9ZqMrrSGEK0JcdTAwMDTtXeB31vB+oVfDs1cos1x1MDAxN52pdVx1MDAxZs9cdTAwMTfl/ZPjQ7ax/0Rf/MOL5tI2j2575Vx1MDAwN8Fx+Fx1MDAxY7x2hVu9uW/HhOqE7WbdO/dr4c3PnotcdTAwMWTvfa7TxF6IPtVu3l/fNLyO7YBI0GbLrfrhsz1GSO/oay/8WYqOPOFfXHUwMDAwwpHAiFRGXHUwMDFhQ0x05+7nJXWEUExSpoXgQOSAYKvNXHUwMDAwx1x1MDAwMlx1MDAwNftcdTAwMDdcXPFaVUSiVdxq/Vx1MDAxYeVr1HrXhG230Wm5bVx1MDAxY7House3R6Y8uvWN51/fhHhQRlx1MDAwZtXxulx1MDAxZFx1MDAwZlx1MDAxYSRKZFTvhL1Ja7vW1YH/xHumUXvrmcZ9XHUwMDEwRHLZXHUwMDEz64N6XHUwMDEz150+/Vx0vadI2NhgP1x1MDAxZl3723opuPpa11+vvJ1rslx1MDAxNJ4v9q776/f0Zl8/zF42audnXHUwMDE3x0vN3X3plVx1MDAxZla/3Kq1/rv8vL/bbjdcdTAwMWZj7b79XHUwMDE2Peh9q+a+6iSVSlx1MDAxM6FcdDdMR4NcdTAwMWP4jfpgXHUwMDFmXHUwMDA0zWo9UuOFmMBcdPz0PX9cdTAwMWM6hoos6FDNmEFRiC6MnfTuXHUwMDFjXHUwMDBiO3R62FE0XHUwMDBmO5o51ExcdTAwMDU7RiehQ80gdKhcdTAwMDZcdTAwMDZcXFGYXHUwMDE4dLK0kDJKiVx1MDAwMjKKXHUwMDE2RmPdbITH/ktXkWTf0Vxy985cdTAwMGae+4arq57YPSvPobfedVxmv/1fvFx1MDAxZjtcdTAwMWXeudtcdTAwMTTv+8xy4F9bNV6s4rN47T5cclx1MDAwZn30Nb1cdTAwMGLu/Fot7j2qKIiLbba3i1x1MDAxOP1m27/2XHUwMDFibnCSJmcu8HJcdTAwMWRcdTAwMTdHdGWhj1FcdTAwMDaGoz6qwujTle3d1fKjvOzcbLeCSiB2XaPn3nNJ7lx1MDAxMK0oVcSAVMz0oY9p6aAx1Fx1MDAxYzVeXHUwMDAyZVx1MDAwND5cZn2o60U8XHUwMDE32lx1MDAwMkEkxaGZsetqy/UtVl7+tvTilqv1tc7S4flyY1xuriu3XbF3V79cclx1MDAxZaTim+et2y/N8mXdrU+g3VBcdTAwMDSbT4dXXHUwMDBmXHUwMDE1oYOV2vPm84/bJz1cdFerNChD0PhHivVBrpZrk8lSKTIhTlx1MDAwNep3YbCnXHUwMDBm/ydwtdlg50Q6MFx1MDAxZLBcdTAwMWKRxHqKq1XGXGJ0+zGv81x1MDAwYvlaPzx+9MPqzW9qur52iJtK+NqYnKV8Z/tcbv007IEhmdijVCilmCpcdTAwMWVcIuZbzznFnqTSQTZhXHKe6uKv39Ey6ShcdTAwMTDCWkJqiFx1MDAwMJ2JPdJ9jY894qBcdTAwMDWQXHUwMDFhKbVcdTAwMDEgRpuUiFx1MDAxMYgjXHUwMDE41VQrJVx1MDAwNIYgbFx1MDAxMJqGI2Y4k6NcdTAwMDSQkV/5qTHs7chfo1x1MDAwM/ZniEbHXHUwMDAxbCd02+GK36j5jet+wd5SIUVYqX1mt4XXXHSHXHUwMDEzjFx1MDAwN1xml0pII6L+7Fx1MDAxYYHqfafb6Tiw1t1cdJAouFx1MDAwNtCJZ/dcdTAwMWG14TLlo7cnk8ZBRlx1MDAwMov/0LlyXHUwMDAxXCJdJlxmuqjSXG7sf1x1MDAxY26iXHUwMDEyQlx1MDAwNW4nXFxt3t35Ifb9QdNvhIN93O3MZVx1MDAwYv1cdTAwMWLPrVxynsWHip9cdTAwMWK0XHUwMDExLdtiP1x1MDAwYot+K0VcdTAwMTDq/tH7/T+/p1+dqdn2ldDpqLmF+PtYoVx1MDAwNDbHXHUwMDA2XHUwMDBm9yxcdTAwMWNcdTAwMTJcdTAwMGJcdTAwMWNwXHUwMDE5c7fDLFxc5UTedbbWn/ZcdTAwMGVEsFX/8m19tbV1PFtcdTAwMGKnhpJcdTAwMGJcdTAwMDKOUlx1MDAwNrhcdTAwMDTgQotcdTAwMDFcdTAwMGKHps1yXHUwMDBmSz9cYmp/pJFcdTAwMTM3cKmRXHUwMDA0RMfeTFx1MDAxOGPaUFxudNY5sCevfHZ5e7N50ZKrrfrB8u7LVc2fXHUwMDAy4Z9cdTAwMTdijujIzoFcdTAwMTmUwlx1MDAxMCOL58DSu3POoYPYYFx1MDAxMXRcdTAwMDaAw2FKwIk501x1MDAxY1ZutFx1MDAxMFIjaZtcdTAwMWEpXHUwMDFmy8ePRcp33IpcdTAwMTf89n1RfV9EsjtNVj7E4lx1MDAwZrLyfkHf4bdoXHUwMDBlM1x1MDAwNyqBXGJcdTAwMTGLm4eBr3a6t7dzclY7P0TQ3Vx1MDAxY79UV59cdTAwMGXUnINPcqTehHGMM20kovvzz1x1MDAwMmk7kUxQJJyKXGIjs4PiKfktqlx1MDAwNMbnJJ7OmI3jkpvkaUM9fttvXHUwMDFmeU+rXHUwMDE3661V9+jub5RRXHUwMDEyLCejhOacauQ8xTNK6d0559hcdTAwMDHlsGzscDot7EiThE7SdXHKXGaGXHUwMDA1ZnpzN9NzXW9Jmim7rSFcdTAwMDZ/0G1FQuZcIi4zkcQgliZcdTAwMWGAnNboy/goUVY+eZ7XPFx1MDAxMoZZVDFUY2lcdTAwMTBYnPZBjlx1MDAwMzhcdTAwMWPhyKnmiEnGP1xmcYDYNpJcdTAwMTjOXGbjUsTuXHUwMDE0+S7pUINcdTAwMDNiU+tKMMVcdTAwMDfxSFx1MDAxObOeN25cdTAwMTimm0ZcdTAwMWHLWcS6tFBcdTAwMWGpcMpcdTAwMDZcdTAwMDNSMFJcdTAwMDB6d8K0jl3xM2WzZONcdTAwMDTCKJGK2excdTAwMWJRSiZcdTAwMWW+UFx1MDAxZSmfb8aEXHUwMDAywOFlaDhcdTAwMTUnPDJH/UJcdE1cdTAwMTWnwIBcdTAwMThhM2BcdJk+U1x1MDAxYWkpU7XtK6HUUXNcdTAwMGLx99FtXHUwMDFidlxcpm0jXGJ5SiFS02G2LZ9fzatt48xBViVcdTAwMTRcdTAwMWE3XG5aRqHuq20zXHUwMDBl2jeBJyQ3mis1INhkjVx1MDAxYva3wpDbKIRaynSVQLyitlM0XHUwMDFlXHUwMDA2JY5f8zPBRJTQXHUwMDA0Ufr/1s2aeodcdTAwMTMp0WppJVx1MDAxNGozxC6JMtJcdTAwMWOQMnNjKHInKWQyXHUwMDFmXci45bOSmHFcdTAwMTNGXHUwMDAxt/8p1TLVuKFInFx1MDAxYqIpJ8SumDNJe/uZbFumYttXUqVHtG25qVx1MDAwNqpoJndD5ibRmlIovtqGtOvbX7dcdTAwMGWDXHUwMDEzctmRrdN6eHrS+jL3XHUwMDA2zlx1MDAxOIcx27WMaWl0xO67k4CGOVRqVDc8XHUwMDBm6G6z7VtFMVxyufbt7Vx1MDAxYSGS1o2ylGhcdFx1MDAxMvZcdTAwMGJVRFxuytHQvst+0eH2q/eZkfJcdTAwMGbaf1bNaq3R2Vx1MDAxNPvqWMOB6bxcdTAwMTROnMPJ9cph58vt3XVjjSw/XHR2tr4rP1f+gVwiWcxcdTAwMDJcdTAwMTRGQtTOXHUwMDAwysJ4Su/NOceTXCJcIlx1MDAxN096cnjKT92RlPCHJ/JcdTAwMGZMXCKbVFx1MDAwMqa3noWNoITRWMfyXHUwMDBm1EHDzImWTKBKkdhUTV8+oj+9sNhcdTAwMGL1ndVcdTAwMWJcdTAwMWM5r/bb94Y9++BcdTAwMDb33j9P2vfe90b6qlx1MDAxNy77WurlIVx1MDAwMu+qXHUwMDFmXHUwMDA0I6UphjiL9DRFruzjcXyusyk+XGJcdTAwMGV8hEKJfFx1MDAwYjZcdTAwMTnA1tzOjTdRxGqGPk5TXHUwMDEwXHUwMDEyQCPp6l+CJoh0XHUwMDE05UA018hR9MchXHUwMDE2hIO3oFx1MDAxOHNR5OiKcEhcdTAwMDJcdTAwMThcdTAwMTRyViosYUJahOBcdTAwMWTEM0ib1Fx1MDAwND5cbp4nTvC54eojXHR++NU/OHKXbzfPt063j2twpra93TiZjqgyM2huKTfUXHUwMDE4ZVikyz3GXHJcdTAwMGXaarxCXHUwMDAypZxRNuZcIph8OPeLpDhcdTAwMTeW7uJQXHUwMDAz4TIhXHUwMDEyd5jgikpk91xmLTP/7LmLTL1+PT2o0pNk+JzFqOLgenpAXHUwMDFlaVx1MDAwN7+wfSt7QeXg7GqLPD7Cycry/ZHoXFzuz/1yeux/ooFcYlx1MDAwNmBcZiH9XHUwMDE5XGbQxulcdTAwMDaTimMsqSE7gfH+1fSRsYrsWYKQaHTsXFxyMevF9GflzfJcdTAwMTKtrHjV1epcdTAwMGZf3vDypbgqSuXp4cF6cNsgq/7h3kZwtXfyrHaCidSBWVx1MDAxNtWN/j+aynNmMpFDQWpcdTAwMDPoXHUwMDE0WWHopHfnnHP510qUXGI6/VxcXlx1MDAxMuOg3ZpcdTAwMDJ0XG6WgVx1MDAxMYw7qITY/MtHcvlRtTCVy89/XHUwMDFk2Fx1MDAxMJv/QXVggtHMRTCAPotJw4tcdTAwMDfSO/ri6kbpx1v/oHy+fPz88Lgmzubeb0nmXHUwMDEwZsBcdTAwMTBKpCZU94FcdTAwMGa7XHUwMDAw/Vx1MDAxNmBcdTAwMTQtKFKKWFx1MDAxMDOrKjAlJGKPTbAyZDzH1VDrbnAsXq7Kl1qrs8ZdsIy+5+NcdTAwMWRXbruHpzWobZRltVx1MDAxNXYuqD5mlaO9/Vx0tNtoP/JNODva6ZgnODhZXHRX7vjW53K0gsVcdTAwMTZ/JCpRrOtcdTAwMTkpaZY+/PPuaFx1MDAxNcnDumCOnlxu1otcdTAwMTWBcTvNSmBKa3am7GdnVVx1MDAwMzbER31EXHKYkCxz+sdmgjhBT1tcdTAwMTh4+aZzToEnKXGEsrVCXGZcdTAwMTksVf25L1x0zFx1MDAwMZDKXHUwMDA2yoxzPSjX5Ga3hWNX7DFcdTAwMDWKo1x0gFx1MDAxNFx1MDAxOErqoFx1MDAxOVSaaWWUMDpJf1FAjpxcdTAwMWNmVlx1MDAwMNZFq2CxXGJ78qmvfDpaXHUwMDFhLO/SXHUwMDFjiNLG1lGp+Oqdn2ViXHUwMDE4Llx1MDAxOIGBg1x1MDAwMmCCQ+LZXHUwMDBi5b7yoVvKLO/CuybzcdpB6ElDqVx1MDAxNFx1MDAwNohmn3tyO0uv7Suh0VFjXHUwMDBi8ffxXHUwMDAyXGIhs6e2hUShsG8jRVx1MDAxZGbc7k9b6ujBe3jY2Hrcba1cdLPjXHUwMDFkzdi4XHUwMDE1qP7iaNuwbzU3nLBcdTAwMDFSgdbOXHUwMDAxQrStuELkfmB1a9FF9MxWwaNGzDqA8I8qa1crtdtccvdCfDup1Fx1MDAwM1J396dA9OeGkEM2dFBPXHUwMDEwWECL04L03pxz5FDlmGzkSDkl5Fx1MDAxNKv+UoxcdTAwMDDGSXJyXHUwMDE54/mh47Or/lx1MDAxYWLxP6z6XHUwMDBiVOZcblx1MDAxMqQo1Fx1MDAxOGGKXHUwMDA3w99u7veO/fLVKWeN3VbraYtvm4c5XHUwMDA3XHUwMDFm+mZHcIyDpTJMx6ffX91cdTAwMTaSKGq3YEPk0fhsznTcVjLvhUxcdTAwMDOMgZnvfnS6slG5fdg/uEJd9Y/LnFc3Ksd/pzySjFx1MDAxOH5yMaOmwG1qpTB00rtzzqFcdTAwMDPaMdnQkeDAVKBTrPSLMqa7ZWi/4nzNbGq/htj7Sdd+cWmyIVx1MDAwN5JQu1a1eJCVz53nNYNEmMNcdTAwMTlGuJJcdTAwMTDDgffvXHUwMDE0oFxmdaRcdTAwMTZcdTAwMTjjaoVRp1x1MDAxOZw+mmBcdTAwMDbJOCCMUZJwgy5Jpm0hhFx1MDAwMbe0Y4ZgQHljXHUwMDBiP3r7b0hQo/ixT5c+KpyqsWVd0ih0XHUwMDFjSPPRoEZ7kpSyqsN44tFcdTAwMGJlj/KJZim7qFx1MDAwYjiFXHUwMDE0ofqqw0RCps+UPVrKVGr7Sqhz1NxC/H10s2ZU5s5BjFFcdTAwMWPzUTh4PrWaV6vGucPRbFx1MDAxMSYpKvfAKm5lwDFCITaowivIx+XFke1TxolmVlx1MDAxOMVT4mFcdTAwMDFcdTAwMGW39Y1GIErBJEsmUH2UlCNFx7+uWXst51x1MDAxMnainiB2XGKw5PJLaic9bEZcXOFVaEjUmCVf+WSklF3QlVx1MDAxNEk7SGmFIYBcdTAwMDZQXHUwMDEzwz73itAspbavQXVcdTAwMWXRpuV/K1x1MDAwMIPsJW2ca6kkXHUwMDFiIUI65N5cdTAwMGVpiy1cdTAwMTnenZZbT5VvlVx1MDAxM3ExW8Mmhld7Scfmy4TQXHUwMDAwXHUwMDE4gPTvjqFcdTAwMTh3hMUqdoPdXHUwMDA0M5utuZJ6opZr1/7hSWE3i0+r9lx1MDAxMilcdTAwMGLaIEHHMEzjwtD3bun4YdVe9y7dXGb8h/37L/5hsKb3g43640rRjFx1MDAwM/9xcv+1/nxSP/56015fuffPrlduP1fGgdHsxdVUgFGCY1RQXHUwMDE4T+ndOd94wuA9XHUwMDBmT5xOXHUwMDBlT/nJurTNLpLVXpTYRJ31NCMgasYph7HLvXpLROak4muIv8hcXOEyftFXvjfkMnPdXHUwMDE59jVcdTAwMTOKj7DHobtz0Vxcalxcr9HTXHUwMDAzXb/wNm+bZ8/e3LN8Y3fO0EaBVlQgw+pDL1x1MDAxOEQvVcyWR1x1MDAxYVx1MDAxZV/sPlx1MDAxYm8ojWGESP2+vVx1MDAxYj7MXHUwMDFidiqHoVe+8E7MQV2dtFdum5Xdtb+TN2RcInu/XfRcdTAwMTNcdTAwMTKdoSjuXHLTu3POXHUwMDAxZd1hXHUwMDFloMzkXHUwMDAwNVx0d8hcYjVcdTAwMTQ0+URcdTAwMGI5f1x1MDAxOW84xGF8gDfMzHdRqTPT+Fx1MDAwNlx1MDAxOLWhd/F8V74pm9dcImgllUOUNFx1MDAxYdWKoaPpX1xiqqh0tOXz1NhccnNIzjZH7922wFx1MDAxMVozSbjmWktqTErRoOSv+11cdJTHriZMbuJmP6pcdTAwMTSwWVVBTyXplc8nS32Jc8LRJNtcdEfVXVwiK2M5l7dcdTAwMTSTdIBLpYjm3e9bgTHXgubDutS3XHUwMDE2XHUwMDE0PVx1MDAwMZPC1lxc21R+MuslXHUwMDFjprlB3VVgR1MlJ1x1MDAxOD5T1mspW7W7p1x1MDAxM1pcdTAwMWQ1uFx1MDAxMH9cdTAwMWZzp6PY5PlgSlx1MDAxZs2fVYziu1Tm19/PhJvAcFx1MDAwM8fsvrCCXCI/wchnoJZTULtcdTAwMDeE5Izh8NiKz5x9Yaex0Vx1MDAxMVxiYlBcdTAwMGJgXrn+16PDcufMO79i4erezsHDt6WD2nzsczTq3lx1MDAxM2NxfVtlm8n1XHUwMDE5t7KoXHUwMDExguf07px3PKlcXDzpXHTiaVx1MDAxMlx1MDAxYlx1MDAxZFEgqFx1MDAxZDJe8vPRO1x1MDAxZI3l/H+9nY6GeItJ73SUidlcdTAwMWNcdTAwMGaImJVSk1x1MDAxMVxuXCI6wU1l47D98uOyeaafVn/slDfXZuxcdTAwMDJcdTAwMGKUWVxu4oDgXHUwMDE4fVx1MDAxYrtiU/av61x1MDAxNkY51FZZaqT5hKpY0elkfSCPfcFNXHUwMDBmsbEvOowqqi3t0yNtVjo+Ylx1MDAwMVx1MDAxOYJ6L2JcdTAwMGKvj1t2Sq+KXuogXHTtlPq1vnSHT+Nee6kojX0/8ygoXHKbrSyI9j3lIFx1MDAxZVx1MDAwYko6XHUwMDE2JmVOkZKynnyETVx1MDAwZfjT5Y/VJ7qp1lx1MDAxZlr8eV08Xla/VudcdTAwMWWRhDukXHUwMDFidFx1MDAxM40/419waVx1MDAxYjDMfoFcdTAwMTVgPIbRpCTZlVx1MDAxNkUy0NmAjH+bXrSwJPn1VGg60EqS6UxcdTAwMWVJSsW7PWjxumen1Esxldpep9W0ul557io9hrylRFx1MDAwMup7I2yW/LBTXHUwMDFhJCZxhzpdqL7/IV5RvPBGsVx1MDAxN91W6zjEcelFXHUwMDE3OOJ+7a1zI1FcdTAwMTZcdTAwMWZ873ElReWuui/batcyWFx1MDAxMHrdUOWvhb/+XHUwMDA3KtdcdTAwMDdDIn0= ByteEditor()BitSwitch(7)Label(\"7\") Switch() Switch.Changed( value=True)ByteEditor()BitSwitch(7)Label(\"7\") Switch() BitSwitch.Changed( value=True)BitSwitch.Changed( value=True)Switch.Changed( value=True)A. Switch sends Switch.Changed messageB. BitSwitch responds by sending BitSwitch.Changedto its parent"},{"location":"guide/widgets/#attributes-down","title":"Attributes down","text":"

    We also want the switches to update if the user edits the decimal value.

    Since the switches are children of ByteEditor we can update them by setting their attributes directly. This is an example of \"attributes down\".

    byte03.pyOutput byte03.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.geometry import clamp\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    class BitChanged(Message):\n        \"\"\"Sent when the 'bit' changes.\"\"\"\n\n        def __init__(self, bit: int, value: bool) -> None:\n            super().__init__()\n            self.bit = bit\n            self.value = value\n\n    value = reactive(0)\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n    def watch_value(self, value: bool) -> None:  # (1)!\n        \"\"\"When the value changes we want to set the switch accordingly.\"\"\"\n        self.query_one(Switch).value = value\n\n    def on_switch_changed(self, event: Switch.Changed) -> None:\n        \"\"\"When the switch changes, notify the parent via a message.\"\"\"\n        event.stop()\n        self.value = event.value\n        self.post_message(self.BitChanged(self.bit, event.value))\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    value = reactive(0)\n\n    def validate_value(self, value: int) -> int:  # (2)!\n        \"\"\"Ensure value is between 0 and 255.\"\"\"\n        return clamp(value, 0, 255)\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n    def on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n        \"\"\"When a switch changes, update the value.\"\"\"\n        value = 0\n        for switch in self.query(BitSwitch):\n            value |= switch.value << switch.bit\n        self.query_one(Input).value = str(value)\n\n    def on_input_changed(self, event: Input.Changed) -> None:  # (3)!\n        \"\"\"When the text changes, set the value of the byte.\"\"\"\n        try:\n            self.value = int(event.value or \"0\")\n        except ValueError:\n            pass\n\n    def watch_value(self, value: int) -> None:  # (4)!\n        \"\"\"When self.value changes, update switches.\"\"\"\n        for switch in self.query(BitSwitch):\n            with switch.prevent(BitSwitch.BitChanged):  # (5)!\n                switch.value = bool(value & (1 << switch.bit))  # (6)!\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n
    1. When the BitSwitch's value changed, we want to update the builtin Switch to match.
    2. Ensure the value is in a the range of a byte.
    3. Handle the Input.Changed event when the user modified the value in the input.
    4. When the ByteEditor value changes, update all the switches to match.
    5. Prevent the BitChanged message from being sent.
    6. Because switch is a child, we can set its attributes directly.

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a100\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    • When the user edits the input, the Input widget sends a Changed event, which we handle with on_input_changed by setting self.value, which is a reactive value we added to ByteEditor.
    • If the value has changed, Textual will call watch_value which sets the value of each of the eight switches. Because we are working with children of the ByteEditor, we can set attributes directly without going via a message.
    "},{"location":"guide/workers/","title":"Workers","text":"

    In this chapter we will explore the topic of concurrency and how to use Textual's Worker API to make it easier.

    The Worker API was added in version 0.18.0

    "},{"location":"guide/workers/#concurrency","title":"Concurrency","text":"

    There are many interesting uses for Textual which require reading data from an internet service. When an app requests data from the network it is important that it doesn't prevent the user interface from updating. In other words, the requests should be concurrent (happen at the same time) as the UI updates.

    Managing this concurrency is a tricky topic, in any language or framework. Even for experienced developers, there are gotchas which could make your app lock up or behave oddly. Textual's Worker API makes concurrency far less error prone and easier to reason about.

    "},{"location":"guide/workers/#workers_1","title":"Workers","text":"

    Before we go into detail, let's see an example that demonstrates a common pitfall for apps that make network requests.

    The following app uses httpx to get the current weather for any given city, by making a request to wttr.in.

    weather01.pyweather.tcssOutput weather01.py
    import httpx\nfrom rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        await self.update_weather(message.value)\n\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n
    weather.tcss
    Input {\n    dock: top;\n    width: 100%;\n}\n\n#weather-container {\n    width: 100%;\n    height: 1fr;\n    align: center middle;\n    overflow: auto;\n}\n\n#weather {\n    width: auto;\n    height: auto;\n}\n

    WeatherApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aEnter\u00a0a\u00a0City\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    If you were to run this app, you should see weather information update as you type. But you may find that the input is not as responsive as usual, with a noticeable delay between pressing a key and seeing it echoed in screen. This is because we are making a request to the weather API within a message handler, and the app will not be able to process other messages until the request has completed (which may be anything from a few hundred milliseconds to several seconds later).

    To resolve this we can use the run_worker method which runs the update_weather coroutine (async def function) in the background. Here's the code:

    weather02.py
    import httpx\nfrom rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.run_worker(self.update_weather(message.value), exclusive=True)\n\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    This one line change will make typing as responsive as you would expect from any app.

    The run_worker method schedules a new worker to run update_weather, and returns a Worker object. This happens almost immediately, so it won't prevent other messages from being processed. The update_weather function is now running concurrently, and will finish a second or two later.

    Tip

    The Worker object has a few useful methods on it, but you can often ignore it as we did in weather02.py.

    The call to run_worker also sets exclusive=True which solves an additional problem with concurrent network requests: when pulling data from the network, there is no guarantee that you will receive the responses in the same order as the requests. For instance, if you start typing \"Paris\", you may get the response for \"Pari\" after the response for \"Paris\", which could show the wrong weather information. The exclusive flag tells Textual to cancel all previous workers before starting the new one.

    "},{"location":"guide/workers/#work-decorator","title":"Work decorator","text":"

    An alternative to calling run_worker manually is the work decorator, which automatically generates a worker from the decorated method.

    Let's use this decorator in our weather app:

    weather03.py
    import httpx\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True)\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    The addition of @work(exclusive=True) converts the update_weather coroutine into a regular function which when called will create and start a worker. Note that even though update_weather is an async def function, the decorator means that we don't need to use the await keyword when calling it.

    Tip

    The decorator takes the same arguments as run_worker.

    "},{"location":"guide/workers/#worker-return-values","title":"Worker return values","text":"

    When you run a worker, the return value of the function won't be available until the work has completed. You can check the return value of a worker with the worker.result attribute which will initially be None, but will be replaced with the return value of the function when it completes.

    If you need the return value you can call worker.wait which is a coroutine that will wait for the work to complete. But note that if you do this in a message handler it will also prevent the widget from updating until the worker returns. Often a better approach is to handle worker events which will notify your app when a worker completes, and the return value is available without waiting.

    "},{"location":"guide/workers/#cancelling-workers","title":"Cancelling workers","text":"

    You can cancel a worker at any time before it is finished by calling Worker.cancel. This will raise a CancelledError within the coroutine, and should cause it to exit prematurely.

    "},{"location":"guide/workers/#worker-errors","title":"Worker errors","text":"

    The default behavior when a worker encounters an exception is to exit the app and display the traceback in the terminal. You can also create workers which will not immediately exit on exception, by setting exit_on_error=False on the call to run_worker or the @work decorator.

    "},{"location":"guide/workers/#worker-lifetime","title":"Worker lifetime","text":"

    Workers are managed by a single WorkerManager instance, which you can access via app.workers. This is a container-like object which you iterate over to see your active workers.

    Workers are tied to the DOM node (widget, screen, or app) where they are created. This means that if you remove the widget or pop the screen where they are created, then the tasks will be cleaned up automatically. Similarly if you exit the app, any running tasks will be cancelled.

    Worker objects have a state attribute which will contain a WorkerState enumeration that indicates what the worker is doing at any given time. The state attribute will contain one of the following values:

    Value Description PENDING The worker was created, but not yet started. RUNNING The worker is currently running. CANCELLED The worker was cancelled and is no longer running. ERROR The worker raised an exception, and worker.error will contain the exception. SUCCESS The worker completed successful, and worker.result will contain the return value.

    Workers start with a PENDING state, then go to RUNNING. From there, they will go to CANCELLED, ERROR or SUCCESS.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVbaVPbSlx1MDAxNv2eX8EwX2aqQqf3JVVTU2BcdTAwMWN2YzDLkFevUsKWjWLZciSZ7VX++7uSwdq8gnFMxkmxqFvS7dv33HNud/PXh7W19fChZ69/Xlu37+uW6zR86279Y3T91vZcdTAwMDPH60JcdTAwMTONf1x1MDAwZry+X4973oRhL/j86VPH8tt22HOtuo1unaBvuUHYbzhcdTAwMWWqe51PTmh3gv9GXytWx/5Pz+s0Qlx1MDAxZiUv2bBcdTAwMWJO6PmDd9mu3bG7YVx1MDAwME//XHUwMDAzfl9b+yv+mrLOt+uh1W25dnxD3JRcdTAwMTjIhM5frXjd2FiKXHUwMDE10YRiTIY9nGBcdTAwMWLeXHUwMDE32lxyaG6CzXbSXHUwMDEyXVq3znrXZLtd3dpUXHUwMDBm21xyfSrtRilIXtt0XFy3XHUwMDE2PrhcdTAwMDNXWPWbvp8yKlxifa9tXzqN8Fx1MDAwNtpJ7vrwvoZcdTAwMTXcgFx1MDAwMcNm3+u3brp2XHUwMDEwZG7yelbdXHRcdTAwMWbgmsTDi1x1MDAwMy98Xkuu3MNvgjDEXHUwMDE1JpozKolcImbYXHUwMDFh3U6MRsxAo1CYXHUwMDExKnJmlTxcdTAwMTdmXHUwMDAyzPonjj+JXddWvd1cdTAwMDLjuo2kXHUwMDBmXHUwMDExlnXdVCrpdfc8XFylkZGaYENcdTAwMTjWwrBkWm5sp3VcdTAwMTNCXHUwMDFmRZDWXFxJIdNm2PFsXHUwMDEww6lcdTAwMTBaXHUwMDEwOWyJXt7ba8SR8WfaXd3Gk7ueQyVcdFx1MDAxNvZ05WcyjKh/OVx1MDAxZmTpQMtcdTAwMDRbaN+Hw9GlXCKDXHUwMDFlYm653vbRySU+7SmveVx1MDAxZPh368N+Pz+Ofuzg5tbX3bPbg4rg2K0+bjaCutNcdTAwMGV19i3P77d830s/9+mnZPz9XsNcdTAwMWFcdTAwMDQwkVx1MDAxYVx1MDAwYmaEMJypYbvrdNvQ2O27bnLNq7eTmP+QMrhcdTAwMDC2zPjTiYCbcTgjlElCXHKZXHUwMDFkZqOduViYXHUwMDA1XHUwMDFlJJtFokxwxFx1MDAwNadMa0MkTsVpdDulXHUwMDE0acYphVxizob3/ChcdTAwMGJ9q1x1MDAxYvQsXHUwMDFmXHUwMDAytogzo4q4oiyPJsNcZotiY1x1MDAxZTBlXCKmgJpFXHUwMDA2YDLRXjesOY+DZJ25+sXqOO5DZq7iyFx1MDAwNPdUy5XtvcpO2oWBXHIvjUNRZbpvuk4rXG7e9TpcZsP2M3FcdTAwMWQ6QEfDXHUwMDBlXHUwMDFkp9FIXHUwMDEzTFx1MDAxZGyw4Jn+3iy84PlOy+la7lnOxIlIm0hrVLPxcMOGXG6qUi6fhrcq/966XGKCbvussnlMSuLhuH9cdTAwMTj+WryZ6aymXHUwMDExQFx04lx1MDAwYuBmXGalXHUwMDE5vHFsXHUwMDEwIJFTpVx1MDAxOFx1MDAwNN/rWFxyUHttyzdhNcKYoVLNXHUwMDA1xCWy2rfqZVmwb3T/ZrfU8qonrTPaO1pcdTAwMDKrTXxuXHUwMDE4XHUwMDFjXHUwMDE4uSVlu3K811xid75x9ztcclx1MDAxN/Dcsz0lws7Z8Vx1MDAxN1BBzdOr+uFV6bv1vliYXHUwMDE4OjYtQHxqRjgzM6eF0bO/4mlBYkQ4XHUwMDA2b3NNOc2JXc5cdTAwMThcdTAwMTJcdTAwMWFcdTAwMDNgXHUwMDA1l/p1aWEyXHKLYlwiKNKwXHUwMDAyNc5cck9N2u9Dw6fnlcqyaXhcbo/lafjZxJfTME/JrVx1MDAxY95cdTAwMDRcdTAwMTPGXGLK2Mxw278qNbulSvXm6urkXFxaTJ5WW1x1MDAwZr9cdTAwMTZuXHUwMDA0T8NcdTAwMWI1XG5cdTAwMTjQ4NE0XGakh0BcZnNJoHpTUGy/ioeb1jXGYvEsrOHNUFx1MDAxZq8qXHTLi3ur6e5cXDx6m1++063+5Vx1MDAxMS6fLoEsV4XUOFx1MDAxZLuEXHUwMDAzYWUwwXNw2mhnrjjIXHUwMDE4VVBbMjya1GAukMRU86j4pK9cdTAwMDTZRFYjqfQ/gdaI0Vx1MDAwNv5cdTAwMTP2O/JaabNSKlx1MDAxZlx1MDAxZZa3l8psU7ghz2yJkS/nNkHEONhRoYWSxMzObf2gVz06bzfZ0ebBl1p5V5VZzVl12Fx1MDAwMXsjQoFcdTAwMWSE0YRcdTAwMDNcdTAwMDBz3CZcdTAwMDCUUlGJXHUwMDE1YFLr18BcdTAwMGW4TURF+1vUmIxSRoRKXHJ3pehtyznY3WnKq9ZuhXnNvc6J7LR+zEpvj03F5f79t83H8I5cXO79aFx1MDAwNY1cdTAwMGLzzuiN87FcdTAwMWFSSC4wobOv5Ix25qrjTClEgFtkviqLYaZcZpLcQPmqtVSvhNlEdlOkXGKsUeRmKMSHTs3a70Nu5dPT49OlXHUwMDEy21x1MDAxNGLIXHUwMDEz28DAl5Oa5HJcdTAwMWPYiIZMbrjWc+xT6KteKdxix51wrytkTe5vXf7iXHUwMDA1kuloU1x1MDAwMiNKXHUwMDE0ZYYpo3VKXFxcdTAwMGZYTSHMXHUwMDE0V9Fmm3ntyinHdSzeZjfQRHeLXHUwMDE1Ldno0f1t7+7r8WG5XSv/j11cXJrKJf8/WodcdTAwMTSpdbpcdTAwMDLMXGKIJVx1MDAwZVbMXHUwMDBls5HeXFx1mFx1MDAxOYZcdTAwMTRwt2FcdTAwMDaUY3pjfVCzXHUwMDExRKmJlmSNfu1cdTAwMDbF5JVIWoTWXGJWXHUwMDAzuGtcIomeRyS+XHUwMDE3Vqudl0rlWm2pvDaFXHUwMDFh8rz2bOJEtFxy0D5cdTAwMDJuTI9dIYlyK+irOUq1yds2v1x1MDAwNG16XHUwMDFh2CRmQFx1MDAxNlx1MDAxMMRcbpiBM5JcdTAwMDVcdTAwMWKUcIjA1Vx1MDAxMXSy2EpcckF0S0O0UlxmftIjkFx1MDAwNypcdTAwMTeIj0FCgFxmSDiWuoBEbahkOj2lL6E2+lbU5tbUj6ZcdTAwMWSow2qtVf8udv3b6ld/PlxugrQok3icXHUwMDAz/UFo+eGW02043VZ21E+nxGbZjY/zRb1cdTAwMWa5YFx1MDAwMyNcZlLEKFx1MDAxZS1RR3pcIknEkd+tXpRAkZagilx1MDAxNOFxoi741e42pps0eWdcImdcdTAwMTKEXHUwMDEwN8JoZVx1MDAwNFCE5Fx1MDAwNZM0opoorJRkUUlEdMEo11xuwpLX6TghuL7qOd0w7+LYl5tRRrmxrUa+XHUwMDE1XHUwMDA2lW7Lp55e9MRsXHUwMDEw/ZFcblx1MDAxNZyOXHUwMDFiPPz5z48je2+MxU30KVwiJnneh/T3eTWKXHUwMDFjL1EkOJ5cdTAwMDPIZ19WXHUwMDFljYrVTprgUmSUVlB6Q+LkJLt1w6hBhEJcdTAwMWNySplk9M2Wt/hsXHUwMDAyRVx1MDAxMYh3upyiW2GskvG+tTy59Py27aNcdTAwMTiQ//r3UlXKXHUwMDE0rs+rlJylL1x1MDAxMytqwnaOwVQxSMazXHUwMDAzb/L+1orWXHUwMDA2QktcdTAwMDS9KDGKZlx1MDAwNUl8SEFcdTAwMWLEhVEkOlx1MDAxN/TKQ1xuXHUwMDEzgEdcdEZSYoVcdTAwMTnoJUzFiNUvXCI5YoRcdTAwMDEzsvyxxSdUQtbGMFx1MDAwMjLPTs9cdTAwMTLFyr7TOvH6cqu822w/XFw2N1x1MDAxZZ3z8423rZfnXHUwMDE1K/MoXHUwMDAzwVxyg6mimkPZyLRK9XpcdTAwMTZcdTAwMDZES0HZy4XK5I2mrDk0OlJcdTAwMDRpXHUwMDE5XmhcdTAwMDCzpGBcdTAwMGVFYKRkw1N471xcplx1MDAwMC5ZdFTAsKfD8un7pVx1MDAwMFFmXHUwMDE0Y4Nl6uFgxz9vLFx1MDAwMKNPXHUwMDAxelx1MDAwYlI9avwxbcJcdTAwMTVhkHJm321cdTAwMThccq9cdTAwMTVPvoyKaFx1MDAxYUFKgtjUXHUwMDAw70zyXHUwMDE1nCFQPVxcgNBkmYXJRcueROJM3G3gWlExVzH4XpZl7lx1MDAwNmqibkH8uUtcdTAwMTY+U2RDXvjkTX2Z8oFcdTAwMDJzbM0ho9NcdTAwMWRcdTAwMDC/2cE3eZl4NU9nSmZcdTAwMDBdRFx1MDAxMKi2WeaoSix8XGaBXHUwMDFjXG7131Nd+2bSh1x1MDAxOIOUXHUwMDE22DBcdTAwMDI1XHUwMDA1o4pcdTAwMTexSCTUnURjXG4l0lx1MDAxOPHDuVx1MDAwMv7jYkXFz87Rdui2+4dcdTAwMWLbvavHg1x1MDAwNt1cdTAwMGZvN3bfqfjBXGLIScnon+QwJSbVZ6A1SLSWQyeJjZn0z+R124xFQDWQnomKTlVcdTAwMGLNaVH/cKRcdTAwMTmTRFx1MDAwM5njhMLfp/whSiCjwfWEXHUwMDE4XHUwMDEyXHUwMDBmJ6N+XGZcIuB6XCJcdTAwMDfbedPVz3hcYsatXHUwMDA18C1I/tDxxadcdTAwMDAsQ1Jis2//jlx1MDAwNthqZ2ClXHUwMDE1UpSDqmC4eNhCcI1cZsWYXHUwMDE3Nq1cdTAwMTabgMVMx+OBXHUwMDA0XGbEXHUwMDEz/lx1MDAxZE9abHtd+1x1MDAxZkuVPFP0Ql7yXGZcZnyZ0GF6rM5cdTAwMTGR8Dacz46yyUe8Vlx1MDAxM2VcdTAwMTLYSGGsoWRcdTAwMDRIXHSVXHUwMDA1XHUwMDE5jir55JBcdTAwMDV/I5BcdTAwMTFgKEJcdTAwMTWWUF1CYoM0O0LlXGKo0bnCQGD5fehcdTAwMDFcdTAwMDbh2VBcdTAwMDeJV56NfzONU71t7Vx1MDAxY7Q23E1v1z5o3oqT+0M9527UojROnpmna43JZ5/WsptCoJaJplx1MDAxNGzlXHUwMDAyXHUwMDE0NC/uXG5RJGT0Z708PqKm3/1yy9jwjT5CI1x1MDAwNlU8V0+nb2dYb1x1MDAwMUhcbqKMUVx1MDAxOL5CVDOVfmBcdTAwMDFcdFx1MDAwYlJcdTAwMWNi/DZcdTAwMTOL8iCAa/ZcXDg63Fc7XHUwMDE3gnpD0Z9CcfB45NnseTPBXHUwMDE1YuD1p02mV/+F0LhkaEaUeEXFQZXBkkv6Oy63lO/rdi+MgnKZqmNcbntcdTAwMTfOd1x1MDAwZY1cdTAwMWOA7cNcdTAwMTOc161er1x1MDAxNoL/hulcdTAwMTRmxmk8OSHx2fqtY99tjYqM+Fx1MDAxMz01XHUwMDA2cFx1MDAwNFx1MDAxNTvmqp9cdTAwMWZ+/lxycIktXGYifQ== PENDINGRUNNINGCANCELLEDERRORSUCCESSWorker.start()worker.cancel()Done!Exception"},{"location":"guide/workers/#worker-events","title":"Worker events","text":"

    When a worker changes state, it sends a Worker.StateChanged event to the widget where the worker was created. You can handle this message by defining an on_worker_state_changed event handler. For instance, here is how we might log the state of the worker that updates the weather:

    weather04.py
    import httpx\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True)\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n        \"\"\"Called when the worker state changes.\"\"\"\n        self.log(event)\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    If you run the above code with textual you should see the worker lifetime events logged in the Textual console.

    textual run weather04.py --dev\n
    "},{"location":"guide/workers/#thread-workers","title":"Thread workers","text":"

    In previous examples we used run_worker or the work decorator in conjunction with coroutines. This works well if you are using an async API like httpx, but if your API doesn't support async you may need to use threads.

    What are threads?

    Threads are a form of concurrency supplied by your Operating System. Threads allow your code to run more than a single function simultaneously.

    You can create threads by setting thread=True on the run_worker method or the work decorator. The API for thread workers is identical to async workers, but there are a few differences you need to be aware of when writing code for thread workers.

    The first difference is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

    The second difference is that you can't cancel threads in the same way as coroutines, but you can manually check if the worker was cancelled.

    Let's demonstrate thread workers by replacing httpx with urllib.request (in the standard library). The urllib module is not async aware, so we will need to use threads:

    weather05.py
    from urllib.parse import quote\nfrom urllib.request import Request, urlopen\n\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker, get_current_worker\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True, thread=True)\n    def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        worker = get_current_worker()\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{quote(city)}\"\n            request = Request(url)\n            request.add_header(\"User-agent\", \"CURL\")\n            response_text = urlopen(request).read().decode(\"utf-8\")\n            weather = Text.from_ansi(response_text)\n            if not worker.is_cancelled:\n                self.call_from_thread(weather_widget.update, weather)\n        else:\n            # No city, so just blank out the weather\n            if not worker.is_cancelled:\n                self.call_from_thread(weather_widget.update, \"\")\n\n    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n        \"\"\"Called when the worker state changes.\"\"\"\n        self.log(event)\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    In this example, the update_weather is not asynchronous (i.e. a regular function). The @work decorator has thread=True which makes it a thread worker. Note the use of get_current_worker which the function uses to check if it has been cancelled or not.

    Important

    Textual will raise an exception if you add the work decorator to a regular function without thread=True.

    "},{"location":"guide/workers/#posting-messages","title":"Posting messages","text":"

    Most Textual functions are not thread-safe which means you will need to use call_from_thread to run them from a thread worker. An exception would be post_message which is thread-safe. If your worker needs to make multiple updates to the UI, it is a good idea to send custom messages and let the message handler update the state of the UI.

    "},{"location":"how-to/","title":"How To","text":"

    Welcome to the How To section.

    Here you will find How To articles which cover various topics at a higher level than the Guide or Reference. We will be adding more articles in the future. If there is anything you would like to see covered, open an issue in the Textual repository!

    "},{"location":"how-to/center-things/","title":"Center things","text":"

    If you have ever needed to center something in a web page, you will be glad to know it is much easier in Textual.

    This article discusses a few different ways in which things can be centered, and the differences between them.

    "},{"location":"how-to/center-things/#aligning-widgets","title":"Aligning widgets","text":"

    The align rule will center a widget relative to one or both edges. This rule is applied to a container, and will impact how the container's children are arranged. Let's see this in practice with a trivial app containing a Static widget:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's the output:

    CenterApp Hello,\u00a0World!

    The container of the widget is the screen, which has the align: center middle; rule applied. The center part tells Textual to align in the horizontal direction, and middle tells Textual to align in the vertical direction.

    The output may surprise you. The text appears to be aligned in the middle (i.e. vertical edge), but left aligned on the horizontal. This isn't a bug \u2014 I promise. Let's make a small change to reveal what is happening here. In the next example, we will add a background and a border to our text:

    Tip

    Adding a border is a very good way of visualizing layout issues, if something isn't behaving as you would expect.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\", id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    The static widget will now have a blue background and white border:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note the static widget is as wide as the screen. Since the widget is as wide as its container, there is no room for it to move in the horizontal direction.

    Info

    The align rule applies to widgets, not the text.

    In order to see the center alignment, we will have to make the widget smaller than the width of the screen. Let's set the width of the Static widget to auto, which will make the widget just wide enough to fit the content:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\", id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this now, you should see the widget is aligned on both axis:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#aligning-text","title":"Aligning text","text":"

    In addition to aligning widgets, you may also want to align text. In order to demonstrate the difference, lets update the example with some longer text. We will also set the width of the widget to something smaller, to force the text to wrap.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's what it looks like with longer text:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eCould\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258eterminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u258a \u258eclassified\u00a0address.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note how the widget is centered, but the text within it is flushed to the left edge. Left aligned text is the default, but you can also center the text with the text-align rule. Let's center align the longer text by setting this rule:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        text-align: center;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this, you will see that each line of text is individually centered:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    You can also use text-align to right align text or justify the text (align to both edges).

    "},{"location":"how-to/center-things/#aligning-content","title":"Aligning content","text":"

    There is one last rule that can help us center things. The content-align rule aligns content within a widget. It treats the text as a rectangular region and positions it relative to the space inside a widget's border.

    In order to see why we might need this rule, we need to make the Static widget larger than required to fit the text. Let's set the height of the Static widget to 9 to give the content room to move:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's what it looks like with the larger widget:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Textual aligns a widget's content to the top border by default, which is why the space is below the text. We can tell Textual to align the content to the center by setting content-align: center middle;

    Note

    Strictly speaking, we only need to align the content vertically here (there is no room to move the content left or right) So we could have done content-align-vertical: middle;

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this now, the content will be centered within the widget:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#aligning-multiple-widgets","title":"Aligning multiple widgets","text":"

    It's just as easy to align multiple widgets as it is a single widget. Applying align: center middle; to the parent widget (screen or other container) will align all its children.

    Let's create an example with two widgets. The following code adds two widgets with auto dimensions:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"How about a nice game\", classes=\"words\")\n        yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    This produces the following output:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    We can center both those widgets by applying the align rule as before:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"How about a nice game\", classes=\"words\")\n        yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's the output:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note how the widgets are aligned as if they are a single group. In other words, their position relative to each other didn't change, just their position relative to the screen.

    If you do want to center each widget independently, you can place each widget inside its own container, and set align for those containers. Textual has a builtin Center container for just this purpose.

    Let's wrap our two widgets in a Center container:

    from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            yield Static(\"How about a nice game\", classes=\"words\")\n        with Center():\n            yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this, you will see that the widgets are centered relative to each other, not just the screen:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#summary","title":"Summary","text":"

    Keep the following in mind when you want to center content in Textual:

    • In order to center a widget, it needs to be smaller than its container.
    • The align rule is applied to the parent of the widget you want to center (i.e. the widget's container).
    • The text-align rule aligns text on a line by line basis.
    • The content-align rule aligns content within a widget.
    • Use the Center container if you want to align multiple widgets relative to each other.
    • Add a border if the alignment isn't working as you would expect.

    If you need further help, we are here to help.

    "},{"location":"how-to/design-a-layout/","title":"Design a Layout","text":"

    This article discusses an approach you can take when designing the layout for your applications.

    Textual's layout system is flexible enough to accommodate just about any application design you could conceive of, but it may be hard to know where to start. We will go through a few tips which will help you get over the initial hurdle of designing an application layout.

    "},{"location":"how-to/design-a-layout/#tip-1-make-a-sketch","title":"Tip 1. Make a sketch","text":"

    The initial design of your application is best done with a sketch. You could use a drawing package such as Excalidraw for your sketch, but pen and paper is equally as good.

    Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction).

    For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns.

    Note

    The approach we are discussing here is applicable even if the app you want to build looks nothing like our sketch!

    Here's our sketch:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

    It's rough, but it's all we need.

    Try in Textual-web

    "},{"location":"how-to/design-a-layout/#tip-2-work-outside-in","title":"Tip 2. Work outside in","text":"

    Like a sculpture with a block of marble, it is best to work from the outside towards the center. If your design has fixed elements (like a header, footer, or sidebar), start with those first.

    In our sketch we have a header and footer. Since these are the outermost widgets, we will begin by adding them.

    Tip

    Textual has builtin Header and Footer widgets which you could use in a real application.

    The following example defines an app, a screen, and our header and footer widgets. Since we're starting from scratch and don't have any functionality for our widgets, we are going to use the Placeholder widget to help us visualize our design.

    In a real app, we would replace these placeholders with more useful content.

    layout01.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):  # (1)!\n    pass\n\n\nclass Footer(Placeholder):  # (2)!\n    pass\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")  # (3)!\n        yield Footer(id=\"Footer\")  # (4)!\n\n\nclass LayoutApp(App):\n    def on_mount(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. The Header widget extends Placeholder.
    2. The footer widget extends Placeholder.
    3. Creates the header widget (the id will be displayed within the placeholder widget).
    4. Creates the footer widget.

    LayoutApp #Header

    "},{"location":"how-to/design-a-layout/#tip-3-apply-docks","title":"Tip 3. Apply docks","text":"

    This app works, but the header and footer don't behave as expected. We want both of these widgets to be fixed to an edge of the screen and limited in height. In Textual this is known as docking which you can apply with the dock rule.

    We will dock the header and footer to the top and bottom edges of the screen respectively, by adding a little CSS to the widget classes:

    layout02.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header #Footer

    The DEFAULT_CSS class variable is used to set CSS directly in Python code. We could define these in an external CSS file, but writing the CSS inline like this can be convenient if it isn't too complex.

    When you dock a widget, it reduces the available area for other widgets. This means that Textual will automatically compensate for the 6 additional lines reserved for the header and footer.

    "},{"location":"how-to/design-a-layout/#tip-4-use-fr-units-for-flexible-things","title":"Tip 4. Use FR Units for flexible things","text":"

    After we've added the header and footer, we want the remaining space to be used for the main interface, which will contain the columns in the sketch. This area is flexible (will change according to the size of the terminal), so how do we ensure that it takes up precisely the space needed?

    The simplest way is to use fr units. By setting both the width and height to 1fr, we are telling Textual to divide the space equally amongst the remaining widgets. There is only a single widget, so that widget will fill all of the remaining space.

    Let's make that change.

    layout03.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass ColumnsContainer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    ColumnsContainer {\n        width: 1fr;\n        height: 1fr;\n        border: solid white;\n    }\n    \"\"\"  # (1)!\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        yield ColumnsContainer(id=\"Columns\")\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. Here's where we set the width and height to 1fr. We also add a border just to illustrate the dimensions better.

    LayoutApp #Header \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502#Columns\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 #Footer

    As you can see, the central Columns area will resize with the terminal window.

    "},{"location":"how-to/design-a-layout/#tip-5-use-containers","title":"Tip 5. Use containers","text":"

    Before we add content to the Columns area, we have an opportunity to simplify. Rather than extend Placeholder for our ColumnsContainer widget, we can use one of the builtin containers. A container is simply a widget designed to contain other widgets. Containers are styled with fr units to fill the remaining space so we won't need to add any more CSS.

    Let's replace the ColumnsContainer class in the previous example with a HorizontalScroll container, which also adds an automatic horizontal scrollbar.

    layout04.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        yield HorizontalScroll()  # (1)!\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. The builtin container widget.

    LayoutApp #Header #Footer

    The container will appear as blank space until we add some widgets to it.

    Let's add the columns to the HorizontalScroll. A column is itself a container which will have a vertical scrollbar, so we will define our Column by subclassing VerticalScroll. In a real app, these columns will likely be added dynamically from some kind of configuration, but let's add 4 to visualize the layout.

    We will also define a Tweet placeholder and add a few to each column.

    layout05.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass Tweet(Placeholder):\n    pass\n\n\nclass Column(VerticalScroll):\n    def compose(self) -> ComposeResult:\n        for tweet_no in range(1, 20):\n            yield Tweet(id=f\"Tweet{tweet_no}\")\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        with HorizontalScroll():\n            yield Column()\n            yield Column()\n            yield Column()\n            yield Column()\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header #Tweet1#Tweet1#Tweet1#Tweet1 #Footer

    Note from the output that each Column takes a quarter of the screen width. This happens because Column extends a container which has a width of 1fr.

    It makes more sense for a column in a Twitter / Mastodon client to use a fixed width. Let's set the width of the columns to 32.

    We also want to reduce the height of each \"tweet\". In the real app, you might set the height to \"auto\" so it fits the content, but lets set it to 5 lines for now.

    Here's the final example and a reminder of the sketch.

    layout06.pyOutputSketch
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass Tweet(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Tweet {\n        height: 5;\n        width: 1fr;\n        border: tall $background;\n    }\n    \"\"\"\n\n\nclass Column(VerticalScroll):\n    DEFAULT_CSS = \"\"\"\n    Column {\n        height: 1fr;\n        width: 32;\n        margin: 0 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for tweet_no in range(1, 20):\n            yield Tweet(id=f\"Tweet{tweet_no}\")\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        with HorizontalScroll():\n            yield Column()\n            yield Column()\n            yield Column()\n            yield Column()\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet1\u258e\u258a#Tweet1\u258e\u258a#Tweet1\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u2583\u2583\u258a\u258e\u2583\u2583\u258a\u258e \u258a#Tweet2\u258e\u258a#Tweet2\u258e\u258a#Tweet2\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet3\u258e\u258a#Tweet3\u258e\u258a#Tweet3\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet4\u258e\u258a#Tweet4\u258e\u258a#Tweet4\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet5\u258e\u258a#Tweet5\u258e\u258a#Tweet5\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258c #Footer

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

    A layout like this is a great starting point. In a real app, you would start replacing each of the placeholders with builtin or custom widgets.

    "},{"location":"how-to/design-a-layout/#summary","title":"Summary","text":"

    Layout is the first thing you will tackle when building a Textual app. The following tips will help you get started.

    1. Make a sketch (pen and paper is fine).
    2. Work outside in. Start with the entire space of the terminal, add the outermost content first.
    3. Dock fixed widgets. If the content doesn't move or scroll, you probably want to dock it.
    4. Make use of fr for flexible space within layouts.
    5. Use containers to contain other widgets, particularly if they scroll!

    If you need further help, we are here to help.

    "},{"location":"how-to/render-and-compose/","title":"Render and compose","text":"

    A common question that comes up on the Textual Discord server is what is the difference between render and compose methods on a widget? In this article we will clarify the differences, and use both these methods to build something fun.

    "},{"location":"how-to/render-and-compose/#which-method-to-use","title":"Which method to use?","text":"

    Render and compose are easy to confuse because they both ultimately define what a widget will look like, but they have quite different uses.

    The render method on a widget returns a Rich renderable, which is anything you could print with Rich. The simplest renderable is just text; so render() methods often return a string, but could equally return a Text instance, a Table, or anything else from Rich (or third party library). Whatever is returned from render() will be combined with any styles from CSS and displayed within the widget's borders.

    The compose method is used to build compound widgets (widgets composed of other widgets).

    A general rule of thumb, is that if you implement a compose method, there is no need for a render method because it is the widgets yielded from compose which define how the custom widget will look. However, you can mix these two methods. If you implement both, the render method will set the custom widget's background and compose will add widgets on top of that background.

    "},{"location":"how-to/render-and-compose/#combining-render-and-compose","title":"Combining render and compose","text":"

    Let's look at an example that combines both these methods. We will create a custom widget with a linear gradient as a background. The background will be animated (I did promise fun)!

    render_compose.pyOutput
    from time import time\n\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Container\nfrom textual.renderables.gradient import LinearGradient\nfrom textual.widgets import Static\n\nCOLORS = [\n    \"#881177\",\n    \"#aa3355\",\n    \"#cc6666\",\n    \"#ee9944\",\n    \"#eedd00\",\n    \"#99dd55\",\n    \"#44dd88\",\n    \"#22ccbb\",\n    \"#00bbcc\",\n    \"#0099cc\",\n    \"#3366bb\",\n    \"#663399\",\n]\nSTOPS = [(i / (len(COLORS) - 1), color) for i, color in enumerate(COLORS)]\n\n\nclass Splash(Container):\n    \"\"\"Custom widget that extends Container.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Splash {\n        align: center middle;\n    }\n    Static {\n        width: 40;\n        padding: 2 4;\n    }\n    \"\"\"\n\n    def on_mount(self) -> None:\n        self.auto_refresh = 1 / 30  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Making a splash with Textual!\")  # (2)!\n\n    def render(self) -> RenderableType:\n        return LinearGradient(time() * 90, STOPS)  # (3)!\n\n\nclass SplashApp(App):\n    \"\"\"Simple app to show our custom widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Splash()\n\n\nif __name__ == \"__main__\":\n    app = SplashApp()\n    app.run()\n
    1. Refresh the widget 30 times a second.
    2. Compose our compound widget, which contains a single Static.
    3. Render a linear gradient in the background.

    SplashApp \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580Making\u00a0a\u00a0splash\u00a0with\u00a0Textual!\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580

    The Splash custom widget has a compose method which adds a simple Static widget to display a message. Additionally there is a render method which returns a renderable to fill the background with a gradient.

    Tip

    As fun as this is, spinning animated gradients may be too distracting for most apps!

    "},{"location":"how-to/render-and-compose/#summary","title":"Summary","text":"

    Keep the following in mind when building custom widgets.

    1. Use render to return simple text, or a Rich renderable.
    2. Use compose to create a widget out of other widgets.
    3. If you define both, then render will be used as a background.

    We are here to help!

    "},{"location":"reference/","title":"Reference","text":"

    Welcome to the Textual Reference.

    • CSS Types

      CSS Types are the data types that CSS styles accept in their rules.

      CSS Types Reference

    • Events

      Events are how Textual communicates with your application.

      Events Reference

    • Styles

      All the styles you can use to take your Textual app to the next level.

      Styles Reference

    • Widgets

      How to use the many widgets builtin to Textual.

      Widgets Reference

    "},{"location":"styles/","title":"Styles","text":"

    A reference to Widget styles.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"styles/align/","title":"Align","text":"

    The align style aligns children within a container.

    "},{"location":"styles/align/#syntax","title":"Syntax","text":"
    \nalign: <horizontal> <vertical>;\n\nalign-horizontal: <horizontal>;\nalign-vertical: <vertical>;\n

    The align style takes a <horizontal> followed by a <vertical>.

    You can also set the alignment for each axis individually with align-horizontal and align-vertical.

    "},{"location":"styles/align/#examples","title":"Examples","text":""},{"location":"styles/align/#basic-usage","title":"Basic usage","text":"

    This example contains a simple app with two labels centered on the screen with align: center middle;:

    Outputalign.pyalign.tcss

    AlignApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Vertical\u00a0alignment\u00a0with\u00a0Textual\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Take\u00a0note,\u00a0browsers.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass AlignApp(App):\n    CSS_PATH = \"align.tcss\"\n\n    def compose(self):\n        yield Label(\"Vertical alignment with [b]Textual[/]\", classes=\"box\")\n        yield Label(\"Take note, browsers.\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = AlignApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\n.box {\n    width: 40;\n    height: 5;\n    margin: 1;\n    padding: 1;\n    background: green;\n    color: white 90%;\n    border: heavy white;\n}\n
    "},{"location":"styles/align/#all-alignments","title":"All alignments","text":"

    The next example shows a 3 by 3 grid of containers with text labels. Each label has been aligned differently inside its container, and its text shows its align: ... value.

    Outputalign_all.pyalign_all.tcss

    AlignAllApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502left\u00a0top\u2502\u2502center\u00a0top\u2502\u2502right\u00a0top\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0middle\u2502\u2502center\u00a0middle\u2502\u2502right\u00a0middle\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0bottom\u2502\u2502center\u00a0bottom\u2502\u2502right\u00a0bottom\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass AlignAllApp(App):\n    \"\"\"App that illustrates all alignments.\"\"\"\n\n    CSS_PATH = \"align_all.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Container(Label(\"left top\"), id=\"left-top\")\n        yield Container(Label(\"center top\"), id=\"center-top\")\n        yield Container(Label(\"right top\"), id=\"right-top\")\n        yield Container(Label(\"left middle\"), id=\"left-middle\")\n        yield Container(Label(\"center middle\"), id=\"center-middle\")\n        yield Container(Label(\"right middle\"), id=\"right-middle\")\n        yield Container(Label(\"left bottom\"), id=\"left-bottom\")\n        yield Container(Label(\"center bottom\"), id=\"center-bottom\")\n        yield Container(Label(\"right bottom\"), id=\"right-bottom\")\n\n\nif __name__ == \"__main__\":\n    AlignAllApp().run()\n
    #left-top {\n    /* align: left top; this is the default value and is implied. */\n}\n\n#center-top {\n    align: center top;\n}\n\n#right-top {\n    align: right top;\n}\n\n#left-middle {\n    align: left middle;\n}\n\n#center-middle {\n    align: center middle;\n}\n\n#right-middle {\n    align: right middle;\n}\n\n#left-bottom {\n    align: left bottom;\n}\n\n#center-bottom {\n    align: center bottom;\n}\n\n#right-bottom {\n    align: right bottom;\n}\n\nScreen {\n    layout: grid;\n    grid-size: 3 3;\n    grid-gutter: 1;\n}\n\nContainer {\n    background: $boost;\n    border: solid gray;\n    height: 100%;\n}\n\nLabel {\n    width: auto;\n    height: 1;\n    background: $accent;\n}\n
    "},{"location":"styles/align/#css","title":"CSS","text":"
    /* Align child widgets to the center. */\nalign: center middle;\n/* Align child widget to the top right */\nalign: right top;\n\n/* Change the horizontal alignment of the children of a widget */\nalign-horizontal: right;\n/* Change the vertical alignment of the children of a widget */\nalign-vertical: middle;\n
    "},{"location":"styles/align/#python","title":"Python","text":"
    # Align child widgets to the center\nwidget.styles.align = (\"center\", \"middle\")\n# Align child widgets to the top right\nwidget.styles.align = (\"right\", \"top\")\n\n# Change the horizontal alignment of the children of a widget\nwidget.styles.align_horizontal = \"right\"\n# Change the vertical alignment of the children of a widget\nwidget.styles.align_vertical = \"middle\"\n
    "},{"location":"styles/align/#see-also","title":"See also","text":"
    • content-align to set the alignment of content inside a widget.
    • text-align to set the alignment of text in a widget.
    "},{"location":"styles/background/","title":"Background","text":"

    The background style sets the background color of a widget.

    "},{"location":"styles/background/#syntax","title":"Syntax","text":"
    \nbackground: <color> [<percentage>];\n

    The background style requires a <color> optionally followed by <percentage> to specify the color's opacity (clamped between 0% and 100%).

    "},{"location":"styles/background/#examples","title":"Examples","text":""},{"location":"styles/background/#basic-usage","title":"Basic usage","text":"

    This example creates three widgets and applies a different background to each.

    Outputbackground.pybackground.tcss

    BackgroundApp Widget\u00a01 Widget\u00a02 Widget\u00a03

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BackgroundApp(App):\n    CSS_PATH = \"background.tcss\"\n\n    def compose(self):\n        yield Label(\"Widget 1\", id=\"static1\")\n        yield Label(\"Widget 2\", id=\"static2\")\n        yield Label(\"Widget 3\", id=\"static3\")\n\n\nif __name__ == \"__main__\":\n    app = BackgroundApp()\n    app.run()\n
    Label {\n    width: 100%;\n    height: 1fr;\n    content-align: center middle;\n    color: white;\n}\n\n#static1 {\n    background: red;\n}\n\n#static2 {\n    background: rgb(0, 255, 0);\n}\n\n#static3 {\n    background: hsl(240, 100%, 50%);\n}\n
    "},{"location":"styles/background/#different-opacity-settings","title":"Different opacity settings","text":"

    The next example creates ten widgets laid out side by side to show the effect of setting different percentages for the background color's opacity.

    Outputbackground_transparency.pybackground_transparency.tcss

    BackgroundTransparencyApp 10%20%30%40%50%60%70%80%90%100%

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass BackgroundTransparencyApp(App):\n    \"\"\"Simple app to exemplify different transparency settings.\"\"\"\n\n    CSS_PATH = \"background_transparency.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"10%\", id=\"t10\")\n        yield Static(\"20%\", id=\"t20\")\n        yield Static(\"30%\", id=\"t30\")\n        yield Static(\"40%\", id=\"t40\")\n        yield Static(\"50%\", id=\"t50\")\n        yield Static(\"60%\", id=\"t60\")\n        yield Static(\"70%\", id=\"t70\")\n        yield Static(\"80%\", id=\"t80\")\n        yield Static(\"90%\", id=\"t90\")\n        yield Static(\"100%\", id=\"t100\")\n\n\nif __name__ == \"__main__\":\n    app = BackgroundTransparencyApp()\n    app.run()\n
    #t10 {\n    background: red 10%;\n}\n\n#t20 {\n    background: red 20%;\n}\n\n#t30 {\n    background: red 30%;\n}\n\n#t40 {\n    background: red 40%;\n}\n\n#t50 {\n    background: red 50%;\n}\n\n#t60 {\n    background: red 60%;\n}\n\n#t70 {\n    background: red 70%;\n}\n\n#t80 {\n    background: red 80%;\n}\n\n#t90 {\n    background: red 90%;\n}\n\n#t100 {\n    background: red 100%;\n}\n\nScreen {\n    layout: horizontal;\n}\n\nStatic {\n    height: 100%;\n    width: 1fr;\n    content-align: center middle;\n}\n
    "},{"location":"styles/background/#css","title":"CSS","text":"
    /* Blue background */\nbackground: blue;\n\n/* 20% red background */\nbackground: red 20%;\n\n/* RGB color */\nbackground: rgb(100, 120, 200);\n\n/* HSL color */\nbackground: hsl(290, 70%, 80%);\n
    "},{"location":"styles/background/#python","title":"Python","text":"

    You can use the same syntax as CSS, or explicitly set a Color object for finer-grained control.

    # Set blue background\nwidget.styles.background = \"blue\"\n# Set through HSL model\nwidget.styles.background = \"hsl(351,32%,89%)\"\n\nfrom textual.color import Color\n# Set with a color object by parsing a string\nwidget.styles.background = Color.parse(\"pink\")\nwidget.styles.background = Color.parse(\"#FF00FF\")\n# Set with a color object instantiated directly\nwidget.styles.background = Color(120, 60, 100)\n
    "},{"location":"styles/background/#see-also","title":"See also","text":"
    • color to set the color of text in a widget.
    "},{"location":"styles/border/","title":"Border","text":"

    The border style enables the drawing of a box around a widget.

    A border style may also be applied to individual edges with border-top, border-right, border-bottom, and border-left.

    Note

    border and outline cannot coexist in the same edge of a widget.

    "},{"location":"styles/border/#syntax","title":"Syntax","text":"
    \nborder: [<border>] [<color>] [<percentage>];\n\nborder-top: [<border>] [<color>] [<percentage>];\nborder-right: [<border>] [<color> [<percentage>]];\nborder-bottom: [<border>] [<color> [<percentage>]];\nborder-left: [<border>] [<color> [<percentage>]];\n

    In CSS, the border is set with a border style and a color. Both are optional. An optional percentage may be added to blend the border with the background color.

    In Python, the border is set with a tuple of border style and a color.

    "},{"location":"styles/border/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively:

    textual borders\n

    Alternatively, you can see the examples below.

    "},{"location":"styles/border/#examples","title":"Examples","text":""},{"location":"styles/border/#basic-usage","title":"Basic usage","text":"

    This examples shows three widgets with different border styles.

    Outputborder.pyborder.tcss

    BorderApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0border\u00a0is\u00a0solid\u00a0red\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0border\u00a0is\u00a0dashed\u00a0green\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0border\u00a0is\u00a0tall\u00a0blue\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderApp(App):\n    CSS_PATH = \"border.tcss\"\n\n    def compose(self):\n        yield Label(\"My border is solid red\", id=\"label1\")\n        yield Label(\"My border is dashed green\", id=\"label2\")\n        yield Label(\"My border is tall blue\", id=\"label3\")\n\n\nif __name__ == \"__main__\":\n    app = BorderApp()\n    app.run()\n
    #label1 {\n    background: red 20%;\n    color: red;\n    border: solid red;\n}\n\n#label2 {\n    background: green 20%;\n    color: green;\n    border: dashed green;\n}\n\n#label3 {\n    background: blue 20%;\n    color: blue;\n    border: tall blue;\n}\n\nScreen {\n    background: white;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border/#all-border-types","title":"All border types","text":"

    The next example shows a grid with all the available border types.

    Outputborder_all.pyborder_all.tcss

    AllBordersApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c hkey\u2590inner\u258c\u258couter\u2590 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u2502round\u2502\u2502solid\u2502\u258atall\u258e \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2588thick\u2588\u258fvkey\u2595\u258ewide\u258a \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass AllBordersApp(App):\n    CSS_PATH = \"border_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"ascii\", id=\"ascii\"),\n            Label(\"blank\", id=\"blank\"),\n            Label(\"dashed\", id=\"dashed\"),\n            Label(\"double\", id=\"double\"),\n            Label(\"heavy\", id=\"heavy\"),\n            Label(\"hidden/none\", id=\"hidden\"),\n            Label(\"hkey\", id=\"hkey\"),\n            Label(\"inner\", id=\"inner\"),\n            Label(\"outer\", id=\"outer\"),\n            Label(\"round\", id=\"round\"),\n            Label(\"solid\", id=\"solid\"),\n            Label(\"tall\", id=\"tall\"),\n            Label(\"thick\", id=\"thick\"),\n            Label(\"vkey\", id=\"vkey\"),\n            Label(\"wide\", id=\"wide\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllBordersApp()\n    app.run()\n
    #ascii {\n    border: ascii $accent;\n}\n\n#blank {\n    border: blank $accent;\n}\n\n#dashed {\n    border: dashed $accent;\n}\n\n#double {\n    border: double $accent;\n}\n\n#heavy {\n    border: heavy $accent;\n}\n\n#hidden {\n    border: hidden $accent;\n}\n\n#hkey {\n    border: hkey $accent;\n}\n\n#inner {\n    border: inner $accent;\n}\n\n#outer {\n    border: outer $accent;\n}\n\n#round {\n    border: round $accent;\n}\n\n#solid {\n    border: solid $accent;\n}\n\n#tall {\n    border: tall $accent;\n}\n\n#thick {\n    border: thick $accent;\n}\n\n#vkey {\n    border: vkey $accent;\n}\n\n#wide {\n    border: wide $accent;\n}\n\nGrid {\n    grid-size: 3 5;\n    align: center middle;\n    grid-gutter: 1 2;\n}\n\nLabel {\n    width: 20;\n    height: 3;\n    content-align: center middle;\n}\n
    "},{"location":"styles/border/#borders-and-outlines","title":"Borders and outlines","text":"

    The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

    This example also shows that a widget cannot contain both a border and an outline:

    Outputoutline_vs_border.pyoutline_vs_border.tcss

    OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineBorderApp(App):\n    CSS_PATH = \"outline_vs_border.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, classes=\"outline\")\n        yield Label(TEXT, classes=\"border\")\n        yield Label(TEXT, classes=\"outline border\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineBorderApp()\n    app.run()\n
    Label {\n    height: 8;\n}\n\n.outline {\n    outline: $error round;\n}\n\n.border {\n    border: $success heavy;\n}\n
    "},{"location":"styles/border/#css","title":"CSS","text":"
    /* Set a heavy white border */\nborder: heavy white;\n\n/* Set a red border on the left */\nborder-left: outer red;\n\n/* Set a rounded orange border, 50% opacity. */\nborder: round orange 50%;\n
    "},{"location":"styles/border/#python","title":"Python","text":"
    # Set a heavy white border\nwidget.styles.border = (\"heavy\", \"white\")\n\n# Set a red border on the left\nwidget.styles.border_left = (\"outer\", \"red\")\n
    "},{"location":"styles/border/#see-also","title":"See also","text":"
    • box-sizing to specify how to account for the border in a widget's dimensions.
    • outline to add an outline around the content of a widget.
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_align/","title":"Border-subtitle-align","text":"

    The border-subtitle-align style sets the horizontal alignment for the border subtitle.

    "},{"location":"styles/border_subtitle_align/#syntax","title":"Syntax","text":"
    \nborder-subtitle-align: <horizontal>;\n

    The border-subtitle-align style takes a <horizontal> that determines where the border subtitle is aligned along the top edge of the border. This means that the border corners are always visible.

    "},{"location":"styles/border_subtitle_align/#default","title":"Default","text":"

    The default alignment is right.

    "},{"location":"styles/border_subtitle_align/#examples","title":"Examples","text":""},{"location":"styles/border_subtitle_align/#basic-usage","title":"Basic usage","text":"

    This example shows three labels, each with a different border subtitle alignment:

    Outputborder_subtitle_align.pyborder_subtitle_align.tcss

    BorderSubtitleAlignApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0subtitle\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Right\u00a0>\u00a0\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderSubtitleAlignApp(App):\n    CSS_PATH = \"border_subtitle_align.tcss\"\n\n    def compose(self):\n        lbl = Label(\"My subtitle is on the left.\", id=\"label1\")\n        lbl.border_subtitle = \"< Left\"\n        yield lbl\n\n        lbl = Label(\"My subtitle is centered\", id=\"label2\")\n        lbl.border_subtitle = \"Centered!\"\n        yield lbl\n\n        lbl = Label(\"My subtitle is on the right\", id=\"label3\")\n        lbl.border_subtitle = \"Right >\"\n        yield lbl\n\n\nif __name__ == \"__main__\":\n    app = BorderSubtitleAlignApp()\n    app.run()\n
    #label1 {\n    border: solid $secondary;\n    border-subtitle-align: left;\n}\n\n#label2 {\n    border: dashed $secondary;\n    border-subtitle-align: center;\n}\n\n#label3 {\n    border: tall $secondary;\n    border-subtitle-align: right;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border_subtitle_align/#complete-usage-reference","title":"Complete usage reference","text":"

    This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

    Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

    BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\n\n\ndef make_label_container(  # (11)!\n    text: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\n    lbl = Label(text, id=id)\n    lbl.border_title = border_title\n    lbl.border_subtitle = border_subtitle\n    return Container(lbl)\n\n\nclass BorderSubTitleAlignAll(App[None]):\n    CSS_PATH = \"border_sub_title_align_all.tcss\"\n\n    def compose(self):\n        with Grid():\n            yield make_label_container(  # (1)!\n                \"This is the story of\",\n                \"lbl1\",\n                \"[b]Border [i]title[/i][/]\",\n                \"[u][r]Border[/r] subtitle[/]\",\n            )\n            yield make_label_container(  # (2)!\n                \"a Python\",\n                \"lbl2\",\n                \"[b red]Left, but it's loooooooooooong\",\n                \"[reverse]Center, but it's loooooooooooong\",\n            )\n            yield make_label_container(  # (3)!\n                \"developer that\",\n                \"lbl3\",\n                \"[b i on purple]Left[/]\",\n                \"[r u white on black]@@@[/]\",\n            )\n            yield make_label_container(\n                \"had to fill up\",\n                \"lbl4\",\n                \"\",  # (4)!\n                \"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n            )\n            yield make_label_container(  # (6)!\n                \"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n            )\n            yield make_label_container(  # (7)!\n                \"and ended up redoing it\",\n                \"lbl6\",\n                \"Title\",\n                \"Subtitle\",\n            )\n            yield make_label_container(  # (8)!\n                \"because the first try\",\n                \"lbl7\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (9)!\n                \"had some labels\",\n                \"lbl8\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (10)!\n                \"that were too long.\",\n                \"lbl9\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n\n\nif __name__ == \"__main__\":\n    app = BorderSubTitleAlignAll()\n    app.run()\n
    1. Border (sub)titles can contain nested markup.
    2. Long (sub)titles get truncated and occupy as much space as possible.
    3. (Sub)titles can be stylised with Rich markup.
    4. An empty (sub)title isn't displayed.
    5. The markup can even contain Rich links.
    6. If the widget does not have a border, the title and subtitle are not shown.
    7. When the side borders are not set, the (sub)title will align with the edge of the widget.
    8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
    9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
    10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
    11. An auxiliary function to create labels with border title and subtitle.
    Grid {\n    grid-size: 3 3;\n    align: center middle;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n    align: center middle;\n}\n\n#lbl1 {  /* (1)! */\n    border: vkey $secondary;\n}\n\n#lbl2 {  /* (2)! */\n    border: round $secondary;\n    border-title-align: right;\n    border-subtitle-align: right;\n}\n\n#lbl3 {\n    border: wide $secondary;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl4 {\n    border: ascii $success;\n    border-title-align: center;  /* (3)! */\n    border-subtitle-align: left;\n}\n\n#lbl5 {  /* (4)! */\n    /* No border = no (sub)title. */\n    border: none $success;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl6 {  /* (5)! */\n    border-top: solid $success;\n    border-bottom: solid $success;\n}\n\n#lbl7 {  /* (6)! */\n    border-top: solid $error;\n    border-bottom: solid $error;\n    padding: 1 2;\n    border-subtitle-align: left;\n}\n\n#lbl8 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl9 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: right;\n}\n
    1. The default alignment for the title is left and the default alignment for the subtitle is right.
    2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
    3. Setting the alignment does not affect empty (sub)titles.
    4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
    5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
    6. Naturally, (sub)title positioning is affected by padding.
    "},{"location":"styles/border_subtitle_align/#css","title":"CSS","text":"
    border-subtitle-align: left;\nborder-subtitle-align: center;\nborder-subtitle-align: right;\n
    "},{"location":"styles/border_subtitle_align/#python","title":"Python","text":"
    widget.styles.border_subtitle_align = \"left\"\nwidget.styles.border_subtitle_align = \"center\"\nwidget.styles.border_subtitle_align = \"right\"\n
    "},{"location":"styles/border_subtitle_align/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_background/","title":"Border-subtitle-background","text":"

    The border-subtitle-background style sets the background color of the border_subtitle.

    "},{"location":"styles/border_subtitle_background/#syntax","title":"Syntax","text":"
    \nborder-subtitle-background: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_subtitle_background/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_background/#css","title":"CSS","text":"
    border-subtitle-background: blue;\n
    "},{"location":"styles/border_subtitle_background/#python","title":"Python","text":"
    widget.styles.border_subtitle_background = \"blue\"\n
    "},{"location":"styles/border_subtitle_background/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_color/","title":"Border-subtitle-color","text":"

    The border-subtitle-color style sets the color of the border_subtitle.

    "},{"location":"styles/border_subtitle_color/#syntax","title":"Syntax","text":"
    \nborder-subtitle-color: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_subtitle_color/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_color/#css","title":"CSS","text":"
    border-subtitle-color: red;\n
    "},{"location":"styles/border_subtitle_color/#python","title":"Python","text":"
    widget.styles.border_subtitle_color = \"red\"\n
    "},{"location":"styles/border_subtitle_color/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_style/","title":"Border-subtitle-style","text":"

    The border-subtitle-style style sets the text style of the border_subtitle.

    "},{"location":"styles/border_subtitle_style/#syntax","title":"Syntax","text":"
    \nborder-subtitle-style: <text-style>;\n
    "},{"location":"styles/border_subtitle_style/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_style/#css","title":"CSS","text":"
    border-subtitle-style: bold underline;\n
    "},{"location":"styles/border_subtitle_style/#python","title":"Python","text":"
    widget.styles.border_subtitle_style = \"bold underline\"\n
    "},{"location":"styles/border_subtitle_style/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_align/","title":"Border-title-align","text":"

    The border-title-align style sets the horizontal alignment for the border title.

    "},{"location":"styles/border_title_align/#syntax","title":"Syntax","text":"
    \nborder-title-align: <horizontal>;\n

    The border-title-align style takes a <horizontal> that determines where the border title is aligned along the top edge of the border. This means that the border corners are always visible.

    "},{"location":"styles/border_title_align/#default","title":"Default","text":"

    The default alignment is left.

    "},{"location":"styles/border_title_align/#examples","title":"Examples","text":""},{"location":"styles/border_title_align/#basic-usage","title":"Basic usage","text":"

    This example shows three labels, each with a different border title alignment:

    Outputborder_title_align.pyborder_title_align.tcss

    BorderTitleAlignApp \u250c\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0title\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0title\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u00a0Right\u00a0>\u00a0\u2594\u258e \u258a\u258e \u258aMy\u00a0title\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderTitleAlignApp(App):\n    CSS_PATH = \"border_title_align.tcss\"\n\n    def compose(self):\n        lbl = Label(\"My title is on the left.\", id=\"label1\")\n        lbl.border_title = \"< Left\"\n        yield lbl\n\n        lbl = Label(\"My title is centered\", id=\"label2\")\n        lbl.border_title = \"Centered!\"\n        yield lbl\n\n        lbl = Label(\"My title is on the right\", id=\"label3\")\n        lbl.border_title = \"Right >\"\n        yield lbl\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleAlignApp()\n    app.run()\n
    #label1 {\n    border: solid $secondary;\n    border-title-align: left;\n}\n\n#label2 {\n    border: dashed $secondary;\n    border-title-align: center;\n}\n\n#label3 {\n    border: tall $secondary;\n    border-title-align: right;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border_title_align/#complete-usage-reference","title":"Complete usage reference","text":"

    This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

    Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

    BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\n\n\ndef make_label_container(  # (11)!\n    text: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\n    lbl = Label(text, id=id)\n    lbl.border_title = border_title\n    lbl.border_subtitle = border_subtitle\n    return Container(lbl)\n\n\nclass BorderSubTitleAlignAll(App[None]):\n    CSS_PATH = \"border_sub_title_align_all.tcss\"\n\n    def compose(self):\n        with Grid():\n            yield make_label_container(  # (1)!\n                \"This is the story of\",\n                \"lbl1\",\n                \"[b]Border [i]title[/i][/]\",\n                \"[u][r]Border[/r] subtitle[/]\",\n            )\n            yield make_label_container(  # (2)!\n                \"a Python\",\n                \"lbl2\",\n                \"[b red]Left, but it's loooooooooooong\",\n                \"[reverse]Center, but it's loooooooooooong\",\n            )\n            yield make_label_container(  # (3)!\n                \"developer that\",\n                \"lbl3\",\n                \"[b i on purple]Left[/]\",\n                \"[r u white on black]@@@[/]\",\n            )\n            yield make_label_container(\n                \"had to fill up\",\n                \"lbl4\",\n                \"\",  # (4)!\n                \"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n            )\n            yield make_label_container(  # (6)!\n                \"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n            )\n            yield make_label_container(  # (7)!\n                \"and ended up redoing it\",\n                \"lbl6\",\n                \"Title\",\n                \"Subtitle\",\n            )\n            yield make_label_container(  # (8)!\n                \"because the first try\",\n                \"lbl7\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (9)!\n                \"had some labels\",\n                \"lbl8\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (10)!\n                \"that were too long.\",\n                \"lbl9\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n\n\nif __name__ == \"__main__\":\n    app = BorderSubTitleAlignAll()\n    app.run()\n
    1. Border (sub)titles can contain nested markup.
    2. Long (sub)titles get truncated and occupy as much space as possible.
    3. (Sub)titles can be stylised with Rich markup.
    4. An empty (sub)title isn't displayed.
    5. The markup can even contain Rich links.
    6. If the widget does not have a border, the title and subtitle are not shown.
    7. When the side borders are not set, the (sub)title will align with the edge of the widget.
    8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
    9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
    10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
    11. An auxiliary function to create labels with border title and subtitle.
    Grid {\n    grid-size: 3 3;\n    align: center middle;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n    align: center middle;\n}\n\n#lbl1 {  /* (1)! */\n    border: vkey $secondary;\n}\n\n#lbl2 {  /* (2)! */\n    border: round $secondary;\n    border-title-align: right;\n    border-subtitle-align: right;\n}\n\n#lbl3 {\n    border: wide $secondary;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl4 {\n    border: ascii $success;\n    border-title-align: center;  /* (3)! */\n    border-subtitle-align: left;\n}\n\n#lbl5 {  /* (4)! */\n    /* No border = no (sub)title. */\n    border: none $success;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl6 {  /* (5)! */\n    border-top: solid $success;\n    border-bottom: solid $success;\n}\n\n#lbl7 {  /* (6)! */\n    border-top: solid $error;\n    border-bottom: solid $error;\n    padding: 1 2;\n    border-subtitle-align: left;\n}\n\n#lbl8 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl9 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: right;\n}\n
    1. The default alignment for the title is left and the default alignment for the subtitle is right.
    2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
    3. Setting the alignment does not affect empty (sub)titles.
    4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
    5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
    6. Naturally, (sub)title positioning is affected by padding.
    "},{"location":"styles/border_title_align/#css","title":"CSS","text":"
    border-title-align: left;\nborder-title-align: center;\nborder-title-align: right;\n
    "},{"location":"styles/border_title_align/#python","title":"Python","text":"
    widget.styles.border_title_align = \"left\"\nwidget.styles.border_title_align = \"center\"\nwidget.styles.border_title_align = \"right\"\n
    "},{"location":"styles/border_title_align/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_background/","title":"Border-title-background","text":"

    The border-title-background style sets the background color of the border_title.

    "},{"location":"styles/border_title_background/#syntax","title":"Syntax","text":"
    \nborder-title-background: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_title_background/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_background/#css","title":"CSS","text":"
    border-title-background: blue;\n
    "},{"location":"styles/border_title_background/#python","title":"Python","text":"
    widget.styles.border_title_background = \"blue\"\n
    "},{"location":"styles/border_title_background/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_color/","title":"Border-title-color","text":"

    The border-title-color style sets the color of the border_title.

    "},{"location":"styles/border_title_color/#syntax","title":"Syntax","text":"
    \nborder-title-color: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_title_color/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_color/#css","title":"CSS","text":"
    border-title-color: red;\n
    "},{"location":"styles/border_title_color/#python","title":"Python","text":"
    widget.styles.border_title_color = \"red\"\n
    "},{"location":"styles/border_title_color/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_style/","title":"Border-title-style","text":"

    The border-title-style style sets the text style of the border_title.

    "},{"location":"styles/border_title_style/#syntax","title":"Syntax","text":"
    \nborder-title-style: <text-style>;\n
    "},{"location":"styles/border_title_style/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_style/#css","title":"CSS","text":"
    border-title-style: bold underline;\n
    "},{"location":"styles/border_title_style/#python","title":"Python","text":"
    widget.styles.border_title_style = \"bold underline\"\n
    "},{"location":"styles/border_title_style/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/box_sizing/","title":"Box-sizing","text":"

    The box-sizing style determines how the width and height of a widget are calculated.

    "},{"location":"styles/box_sizing/#syntax","title":"Syntax","text":"
    box-sizing: border-box | content-box;\n
    "},{"location":"styles/box_sizing/#values","title":"Values","text":"Value Description border-box (default) Padding and border are included in the width and height. If you add padding and/or border the widget will not change in size, but you will have less space for content. content-box Padding and border will increase the size of the widget, leaving the content area unaffected."},{"location":"styles/box_sizing/#example","title":"Example","text":"

    Both widgets in this example have the same height (5). The top widget has box-sizing: border-box which means that padding and border reduce the space for content. The bottom widget has box-sizing: content-box which increases the size of the widget to compensate for padding and border.

    Outputbox_sizing.pybox_sizing.tcss

    BoxSizingApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0border-box!\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0content-box!\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass BoxSizingApp(App):\n    CSS_PATH = \"box_sizing.tcss\"\n\n    def compose(self):\n        yield Static(\"I'm using border-box!\", id=\"static1\")\n        yield Static(\"I'm using content-box!\", id=\"static2\")\n\n\nif __name__ == \"__main__\":\n    app = BoxSizingApp()\n    app.run()\n
    #static1 {\n    box-sizing: border-box;\n}\n\n#static2 {\n    box-sizing: content-box;\n}\n\nScreen {\n    background: white;\n    color: black;\n}\n\nApp Static {\n    background: blue 20%;\n    height: 5;\n    margin: 2;\n    padding: 1;\n    border: wide black;\n}\n
    "},{"location":"styles/box_sizing/#css","title":"CSS","text":"
    /* Set box sizing to border-box (default) */\nbox-sizing: border-box;\n\n/* Set box sizing to content-box */\nbox-sizing: content-box;\n
    "},{"location":"styles/box_sizing/#python","title":"Python","text":"
    # Set box sizing to border-box (default)\nwidget.box_sizing = \"border-box\"\n\n# Set box sizing to content-box\nwidget.box_sizing = \"content-box\"\n
    "},{"location":"styles/box_sizing/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    • padding to add spacing around the content of a widget.
    "},{"location":"styles/color/","title":"Color","text":"

    The color style sets the text color of a widget.

    "},{"location":"styles/color/#syntax","title":"Syntax","text":"
    \ncolor: (<color> | auto) [<percentage>];\n

    The color style requires a <color> followed by an optional <percentage> to specify the color's opacity.

    You can also use the special value of \"auto\" in place of a color. This tells Textual to automatically select either white or black text for best contrast against the background.

    "},{"location":"styles/color/#examples","title":"Examples","text":""},{"location":"styles/color/#basic-usage","title":"Basic usage","text":"

    This example sets a different text color for each of three different widgets.

    Outputcolor.pycolor.tcss

    ColorApp I'm\u00a0red! I'm\u00a0rgb(0,\u00a0255,\u00a00)! I'm\u00a0hsl(240,\u00a0100%,\u00a050%)!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ColorApp(App):\n    CSS_PATH = \"color.tcss\"\n\n    def compose(self):\n        yield Label(\"I'm red!\", id=\"label1\")\n        yield Label(\"I'm rgb(0, 255, 0)!\", id=\"label2\")\n        yield Label(\"I'm hsl(240, 100%, 50%)!\", id=\"label3\")\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n
    Label {\n    height: 1fr;\n    content-align: center middle;\n    width: 100%;\n}\n\n#label1 {\n    color: red;\n}\n\n#label2 {\n    color: rgb(0, 255, 0);\n}\n\n#label3 {\n    color: hsl(240, 100%, 50%);\n}\n
    "},{"location":"styles/color/#auto","title":"Auto","text":"

    The next example shows how auto chooses between a lighter or a darker text color to increase the contrast and improve readability.

    Outputcolor_auto.pycolor_auto.tcss

    ColorApp The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ColorApp(App):\n    CSS_PATH = \"color_auto.tcss\"\n\n    def compose(self):\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl1\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl2\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl3\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl4\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl5\")\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n
    Label {\n    color: auto 80%;\n    content-align: center middle;\n    height: 1fr;\n    width: 100%;\n}\n\n#lbl1 {\n    background: red 80%;\n}\n\n#lbl2 {\n    background: yellow 80%;\n}\n\n#lbl3 {\n    background: blue 80%;\n}\n\n#lbl4 {\n    background: pink 80%;\n}\n\n#lbl5 {\n    background: green 80%;\n}\n
    "},{"location":"styles/color/#css","title":"CSS","text":"
    /* Blue text */\ncolor: blue;\n\n/* 20% red text */\ncolor: red 20%;\n\n/* RGB color */\ncolor: rgb(100, 120, 200);\n\n/* Automatically choose color with suitable contrast for readability */\ncolor: auto;\n
    "},{"location":"styles/color/#python","title":"Python","text":"

    You can use the same syntax as CSS, or explicitly set a Color object.

    # Set blue text\nwidget.styles.color = \"blue\"\n\nfrom textual.color import Color\n# Set with a color object\nwidget.styles.color = Color.parse(\"pink\")\n
    "},{"location":"styles/color/#see-also","title":"See also","text":"
    • background to set the background color in a widget.
    "},{"location":"styles/content_align/","title":"Content-align","text":"

    The content-align style aligns content inside a widget.

    "},{"location":"styles/content_align/#syntax","title":"Syntax","text":"
    \ncontent-align: <horizontal> <vertical>;\n\ncontent-align-horizontal: <horizontal>;\ncontent-align-vertical: <vertical>;\n

    The content-align style takes a <horizontal> followed by a <vertical>.

    You can specify the alignment of content on both the horizontal and vertical axes at the same time, or on each of the axis separately. To specify content alignment on a single axis, use the respective style and type:

    • content-align-horizontal takes a <horizontal> and does alignment along the horizontal axis; and
    • content-align-vertical takes a <vertical> and does alignment along the vertical axis.
    "},{"location":"styles/content_align/#examples","title":"Examples","text":""},{"location":"styles/content_align/#basic-usage","title":"Basic usage","text":"

    This first example shows three labels stacked vertically, each with different content alignments.

    Outputcontent_align.pycontent_align.tcss

    ContentAlignApp With\u00a0content-align\u00a0you\u00a0can... ...Easily\u00a0align\u00a0content... ...Horizontally\u00a0and\u00a0vertically!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ContentAlignApp(App):\n    CSS_PATH = \"content_align.tcss\"\n\n    def compose(self):\n        yield Label(\"With [i]content-align[/] you can...\", id=\"box1\")\n        yield Label(\"...[b]Easily align content[/]...\", id=\"box2\")\n        yield Label(\"...Horizontally [i]and[/] vertically!\", id=\"box3\")\n\n\nif __name__ == \"__main__\":\n    app = ContentAlignApp()\n    app.run()\n
    #box1 {\n    content-align: left top;\n    background: red;\n}\n\n#box2 {\n    content-align-horizontal: center;\n    content-align-vertical: middle;\n    background: green;\n}\n\n#box3 {\n    content-align: right bottom;\n    background: blue;\n}\n\nLabel {\n    width: 100%;\n    height: 1fr;\n    padding: 1;\n    color: white;\n}\n
    "},{"location":"styles/content_align/#all-content-alignments","title":"All content alignments","text":"

    The next example shows a 3 by 3 grid of labels. Each label has its text aligned differently.

    Outputcontent_align_all.pycontent_align_all.tcss

    AllContentAlignApp left\u00a0topcenter\u00a0topright\u00a0top left\u00a0middlecenter\u00a0middleright\u00a0middle left\u00a0bottomcenter\u00a0bottomright\u00a0bottom

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass AllContentAlignApp(App):\n    CSS_PATH = \"content_align_all.tcss\"\n\n    def compose(self):\n        yield Label(\"left top\", id=\"left-top\")\n        yield Label(\"center top\", id=\"center-top\")\n        yield Label(\"right top\", id=\"right-top\")\n        yield Label(\"left middle\", id=\"left-middle\")\n        yield Label(\"center middle\", id=\"center-middle\")\n        yield Label(\"right middle\", id=\"right-middle\")\n        yield Label(\"left bottom\", id=\"left-bottom\")\n        yield Label(\"center bottom\", id=\"center-bottom\")\n        yield Label(\"right bottom\", id=\"right-bottom\")\n\n\nif __name__ == \"__main__\":\n    app = AllContentAlignApp()\n    app.run()\n
    #left-top {\n    /* content-align: left top; this is the default implied value. */\n}\n#center-top {\n    content-align: center top;\n}\n#right-top {\n    content-align: right top;\n}\n#left-middle {\n    content-align: left middle;\n}\n#center-middle {\n    content-align: center middle;\n}\n#right-middle {\n    content-align: right middle;\n}\n#left-bottom {\n    content-align: left bottom;\n}\n#center-bottom {\n    content-align: center bottom;\n}\n#right-bottom {\n    content-align: right bottom;\n}\n\nScreen {\n    layout: grid;\n    grid-size: 3 3;\n    grid-gutter: 1;\n}\n\nLabel {\n    width: 100%;\n    height: 100%;\n    background: $primary;\n}\n
    "},{"location":"styles/content_align/#css","title":"CSS","text":"
    /* Align content in the very center of a widget */\ncontent-align: center middle;\n/* Align content at the top right of a widget */\ncontent-align: right top;\n\n/* Change the horizontal alignment of the content of a widget */\ncontent-align-horizontal: right;\n/* Change the vertical alignment of the content of a widget */\ncontent-align-vertical: middle;\n
    "},{"location":"styles/content_align/#python","title":"Python","text":"
    # Align content in the very center of a widget\nwidget.styles.content_align = (\"center\", \"middle\")\n# Align content at the top right of a widget\nwidget.styles.content_align = (\"right\", \"top\")\n\n# Change the horizontal alignment of the content of a widget\nwidget.styles.content_align_horizontal = \"right\"\n# Change the vertical alignment of the content of a widget\nwidget.styles.content_align_vertical = \"middle\"\n
    "},{"location":"styles/content_align/#see-also","title":"See also","text":"
    • align to set the alignment of children widgets inside a container.
    • text-align to set the alignment of text in a widget.
    "},{"location":"styles/display/","title":"Display","text":"

    The display style defines whether a widget is displayed or not.

    "},{"location":"styles/display/#syntax","title":"Syntax","text":"
    display: block | none;\n
    "},{"location":"styles/display/#values","title":"Values","text":"Value Description block (default) Display the widget as normal. none The widget is not displayed and space will no longer be reserved for it."},{"location":"styles/display/#example","title":"Example","text":"

    Note that the second widget is hidden by adding the \"remove\" class which sets the display style to none.

    Outputdisplay.pydisplay.tcss

    DisplayApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass DisplayApp(App):\n    CSS_PATH = \"display.tcss\"\n\n    def compose(self):\n        yield Static(\"Widget 1\")\n        yield Static(\"Widget 2\", classes=\"remove\")\n        yield Static(\"Widget 3\")\n\n\nif __name__ == \"__main__\":\n    app = DisplayApp()\n    app.run()\n
    Screen {\n    background: green;\n}\n\nStatic {\n    height: 5;\n    background: white;\n    color: blue;\n    border: heavy blue;\n}\n\nStatic.remove {\n    display: none;\n}\n
    "},{"location":"styles/display/#css","title":"CSS","text":"
    /* Widget is shown */\ndisplay: block;\n\n/* Widget is not shown */\ndisplay: none;\n
    "},{"location":"styles/display/#python","title":"Python","text":"
    # Hide the widget\nself.styles.display = \"none\"\n\n# Show the widget again\nself.styles.display = \"block\"\n

    There is also a shortcut to show / hide a widget. The display property on Widget may be set to True or False to show or hide the widget.

    # Hide the widget\nwidget.display = False\n\n# Show the widget\nwidget.display = True\n
    "},{"location":"styles/display/#see-also","title":"See also","text":"
    • visibility to specify whether a widget is visible or not.
    "},{"location":"styles/dock/","title":"Dock","text":"

    The dock style is used to fix a widget to the edge of a container (which may be the entire terminal window).

    "},{"location":"styles/dock/#syntax","title":"Syntax","text":"
    dock: bottom | left | right | top;\n

    The option chosen determines the edge to which the widget is docked.

    "},{"location":"styles/dock/#examples","title":"Examples","text":""},{"location":"styles/dock/#basic-usage","title":"Basic usage","text":"

    The example below shows a left docked sidebar. Notice that even though the content is scrolled, the sidebar remains fixed.

    Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

    DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout1_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n
    "},{"location":"styles/dock/#advanced-usage","title":"Advanced usage","text":"

    The second example shows how one can use full-width or full-height containers to dock labels to the edges of a larger container. The labels will remain in that position (docked) even if the container they are in scrolls horizontally and/or vertically.

    Outputdock_all.pydock_all.tcss

    DockAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502top\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502leftright\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502bottom\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass DockAllApp(App):\n    CSS_PATH = \"dock_all.tcss\"\n\n    def compose(self):\n        yield Container(\n            Container(Label(\"left\"), id=\"left\"),\n            Container(Label(\"top\"), id=\"top\"),\n            Container(Label(\"right\"), id=\"right\"),\n            Container(Label(\"bottom\"), id=\"bottom\"),\n            id=\"big_container\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = DockAllApp()\n    app.run()\n
    #left {\n    dock: left;\n    height: 100%;\n    width: auto;\n    align-vertical: middle;\n}\n#top {\n    dock: top;\n    height: auto;\n    width: 100%;\n    align-horizontal: center;\n}\n#right {\n    dock: right;\n    height: 100%;\n    width: auto;\n    align-vertical: middle;\n}\n#bottom {\n    dock: bottom;\n    height: auto;\n    width: 100%;\n    align-horizontal: center;\n}\n\nScreen {\n    align: center middle;\n}\n\n#big_container {\n    width: 75%;\n    height: 75%;\n    border: round white;\n}\n
    "},{"location":"styles/dock/#css","title":"CSS","text":"
    dock: bottom;  /* Docks on the bottom edge of the parent container. */\ndock: left;    /* Docks on the   left edge of the parent container. */\ndock: right;   /* Docks on the  right edge of the parent container. */\ndock: top;     /* Docks on the    top edge of the parent container. */\n
    "},{"location":"styles/dock/#python","title":"Python","text":"
    widget.styles.dock = \"bottom\"  # Dock bottom.\nwidget.styles.dock = \"left\"    # Dock   left.\nwidget.styles.dock = \"right\"   # Dock  right.\nwidget.styles.dock = \"top\"     # Dock    top.\n
    "},{"location":"styles/dock/#see-also","title":"See also","text":"
    • The layout guide section on docking.
    "},{"location":"styles/height/","title":"Height","text":"

    The height style sets a widget's height.

    "},{"location":"styles/height/#syntax","title":"Syntax","text":"
    \nheight: <scalar>;\n

    The height style needs a <scalar> to determine the vertical length of the widget. By default, it sets the height of the content area, but if box-sizing is set to border-box it sets the height of the border area.

    "},{"location":"styles/height/#examples","title":"Examples","text":""},{"location":"styles/height/#basic-usage","title":"Basic usage","text":"

    This examples creates a widget with a height of 50% of the screen.

    Outputheight.pyheight.tcss

    HeightApp Widget

    from textual.app import App\nfrom textual.widget import Widget\n\n\nclass HeightApp(App):\n    CSS_PATH = \"height.tcss\"\n\n    def compose(self):\n        yield Widget()\n\n\nif __name__ == \"__main__\":\n    app = HeightApp()\n    app.run()\n
    Screen > Widget {\n    background: green;\n    height: 50%;\n    color: white;\n}\n
    "},{"location":"styles/height/#all-height-formats","title":"All height formats","text":"

    The next example creates a series of wide widgets with heights set with different units. Open the CSS file tab to see the comments that explain how each height is computed. (The output includes a vertical ruler on the right to make it easier to check the height of each widget.)

    Outputheight_comparison.pyheight_comparison.tcss

    HeightComparisonApp #cells\u00b7 \u00b7 \u00b7 #percent\u00b7 \u2022 \u00b7 #w\u00b7 \u00b7 \u00b7 \u2022 #h\u00b7 \u00b7 \u00b7 \u00b7 #vw\u2022 \u00b7 \u00b7 \u00b7 #vh\u00b7 \u2022 #auto\u00b7 #fr1\u00b7 #fr2\u00b7 \u00b7

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Label, Placeholder, Static\n\n\nclass Ruler(Static):\n    def compose(self):\n        ruler_text = \"\u00b7\\n\u00b7\\n\u00b7\\n\u00b7\\n\u2022\\n\" * 100\n        yield Label(ruler_text)\n\n\nclass HeightComparisonApp(App):\n    CSS_PATH = \"height_comparison.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(id=\"cells\"),  # (1)!\n            Placeholder(id=\"percent\"),\n            Placeholder(id=\"w\"),\n            Placeholder(id=\"h\"),\n            Placeholder(id=\"vw\"),\n            Placeholder(id=\"vh\"),\n            Placeholder(id=\"auto\"),\n            Placeholder(id=\"fr1\"),\n            Placeholder(id=\"fr2\"),\n        )\n        yield Ruler()\n\n\nif __name__ == \"__main__\":\n    app = HeightComparisonApp()\n    app.run()\n
    1. The id of the placeholder identifies which unit will be used to set the height of the widget.
    #cells {\n    height: 2;       /* (1)! */\n}\n#percent {\n    height: 12.5%;   /* (2)! */\n}\n#w {\n    height: 5w;      /* (3)! */\n}\n#h {\n    height: 12.5h;   /* (4)! */\n}\n#vw {\n    height: 6.25vw;  /* (5)! */\n}\n#vh {\n    height: 12.5vh;  /* (6)! */\n}\n#auto {\n    height: auto;    /* (7)! */\n}\n#fr1 {\n    height: 1fr;     /* (8)! */\n}\n#fr2 {\n    height: 2fr;     /* (9)! */\n}\n\nScreen {\n    layers: ruler;\n    overflow: hidden;\n}\n\nRuler {\n    layer: ruler;\n    dock: right;\n    width: 1;\n    background: $accent;\n}\n
    1. This sets the height to 2 lines.
    2. This sets the height to 12.5% of the space made available by the container. The container is 24 lines tall, so 12.5% of 24 is 3.
    3. This sets the height to 5% of the width of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the width of the VerticalScroll is 80 and 5% of 80 is 4.
    4. This sets the height to 12.5% of the height of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the height of the VerticalScroll is 24 and 12.5% of 24 is 3.
    5. This sets the height to 6.25% of the viewport width, which is 80. 6.25% of 80 is 5.
    6. This sets the height to 12.5% of the viewport height, which is 24. 12.5% of 24 is 3.
    7. This sets the height of the placeholder to be the optimal size that fits the content without scrolling. Because the content only spans one line, the placeholder has its height set to 1.
    8. This sets the height to 1fr, which means this placeholder will have half the height of a placeholder with 2fr.
    9. This sets the height to 2fr, which means this placeholder will have twice the height of a placeholder with 1fr.
    "},{"location":"styles/height/#css","title":"CSS","text":"
    /* Explicit cell height */\nheight: 10;\n\n/* Percentage height */\nheight: 50%;\n\n/* Automatic height */\nheight: auto\n
    "},{"location":"styles/height/#python","title":"Python","text":"
    self.styles.height = 10  # Explicit cell height can be an int\nself.styles.height = \"50%\"\nself.styles.height = \"auto\"\n
    "},{"location":"styles/height/#see-also","title":"See also","text":"
    • max-height and min-height to limit the height of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/keyline/","title":"Keyline","text":"

    The keyline style is applied to a container and will draw lines around child widgets.

    A keyline is superficially like the border rule, but rather than draw inside the widget, a keyline is drawn outside of the widget's border. Additionally, unlike border, keylines can overlap and cross to create dividing lines between widgets.

    Because keylines are drawn in the widget's margin, you will need to apply the margin or grid-gutter rule to see the effect.

    "},{"location":"styles/keyline/#syntax","title":"Syntax","text":"
    \nkeyline: [<keyline>] [<color>];\n
    "},{"location":"styles/keyline/#examples","title":"Examples","text":""},{"location":"styles/keyline/#horizontal-keyline","title":"Horizontal Keyline","text":"

    The following examples shows a simple horizontal layout with a thin keyline.

    Outputkeyline.pykeyline.tcss

    KeylineApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Placeholder\u2502Placeholder\u2502Placeholder\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass KeylineApp(App):\n    CSS_PATH = \"keyline_horizontal.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Placeholder()\n            yield Placeholder()\n            yield Placeholder()\n\n\nif __name__ == \"__main__\":\n    app = KeylineApp()\n    app.run()\n
    Placeholder {\n    margin: 1;\n    width: 1fr;\n}\n\nHorizontal {\n    keyline: thin $secondary;\n}\n
    "},{"location":"styles/keyline/#grid-keyline","title":"Grid keyline","text":"

    The following examples shows a grid layout with a heavy keyline.

    Outputkeyline.pykeyline.tcss

    KeylineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503\u2503 \u2503\u2503\u2503 \u2503#foo\u2503\u2503 \u2503\u2503\u2503 \u2503\u2503\u2503 \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b#bar\u2503 \u2503\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2503Placeholder\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b \u2503\u2503 \u2503\u2503 \u2503#baz\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass KeylineApp(App):\n    CSS_PATH = \"keyline.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Grid():\n            yield Placeholder(id=\"foo\")\n            yield Placeholder(id=\"bar\")\n            yield Placeholder()\n            yield Placeholder(classes=\"hidden\")\n            yield Placeholder(id=\"baz\")\n\n\nif __name__ == \"__main__\":\n    KeylineApp().run()\n
    Grid {\n    grid-size: 3 3;\n    grid-gutter: 1;\n    padding: 2 3;\n    keyline: heavy green;\n}\nPlaceholder {\n    height: 1fr;      \n}\n.hidden {\n    visibility: hidden;\n} \n#foo {\n    column-span: 2;\n}\n#bar {\n    row-span: 2;        \n}\n#baz {\n    column-span:3;\n}\n
    "},{"location":"styles/keyline/#css","title":"CSS","text":"
    /* Set a thin green keyline */\n/* Note: Must be set on a container or a widget with a layout. */\nkeyline: thin green;\n
    "},{"location":"styles/keyline/#python","title":"Python","text":"

    You can set a keyline in Python with a tuple of type and color:

    widget.styles.keyline = (\"thin\", \"green\")\n
    "},{"location":"styles/keyline/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    "},{"location":"styles/layer/","title":"Layer","text":"

    The layer style defines the layer a widget belongs to.

    "},{"location":"styles/layer/#syntax","title":"Syntax","text":"
    \nlayer: <name>;\n

    The layer style accepts a <name> that defines the layer this widget belongs to. This <name> must correspond to a <name> that has been defined in a layers style by an ancestor of this widget.

    More information on layers can be found in the guide.

    Warning

    Using a <name> that hasn't been defined in a layers declaration of an ancestor of this widget has no effect.

    "},{"location":"styles/layer/#example","title":"Example","text":"

    In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"styles/layer/#css","title":"CSS","text":"
    /* Draw the widget on the layer called 'below' */\nlayer: below;\n
    "},{"location":"styles/layer/#python","title":"Python","text":"
    # Draw the widget on the layer called 'below'\nwidget.styles.layer = \"below\"\n
    "},{"location":"styles/layer/#see-also","title":"See also","text":"
    • The layout guide section on layers.
    • layers to define an ordered set of layers.
    "},{"location":"styles/layers/","title":"Layers","text":"

    The layers style allows you to define an ordered set of layers.

    "},{"location":"styles/layers/#syntax","title":"Syntax","text":"
    \nlayers: <name>+;\n

    The layers style accepts one or more <name> that define the layers that the widget is aware of, and the order in which they will be painted on the screen.

    The values used here can later be referenced using the layer property. The layers defined first in the list are drawn under the layers that are defined later in the list.

    More information on layers can be found in the guide.

    "},{"location":"styles/layers/#example","title":"Example","text":"

    In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"styles/layers/#css","title":"CSS","text":"
    /* Bottom layer is called 'below', layer above it is called 'above' */\nlayers: below above;\n
    "},{"location":"styles/layers/#python","title":"Python","text":"
    # Bottom layer is called 'below', layer above it is called 'above'\nwidget.style.layers = (\"below\", \"above\")\n
    "},{"location":"styles/layers/#see-also","title":"See also","text":"
    • The layout guide section on layers.
    • layer to set the layer a widget belongs to.
    "},{"location":"styles/layout/","title":"Layout","text":"

    The layout style defines how a widget arranges its children.

    "},{"location":"styles/layout/#syntax","title":"Syntax","text":"
    \nlayout: grid | horizontal | vertical;\n

    The layout style takes an option that defines how child widgets will be arranged, as per the table shown below.

    "},{"location":"styles/layout/#values","title":"Values","text":"Value Description grid Child widgets will be arranged in a grid. horizontal Child widgets will be arranged along the horizontal axis, from left to right. vertical (default) Child widgets will be arranged along the vertical axis, from top to bottom.

    See the layout guide for more information.

    "},{"location":"styles/layout/#example","title":"Example","text":"

    Note how the layout style affects the arrangement of widgets in the example below. To learn more about the grid layout, you can see the layout guide or the grid reference.

    Outputlayout.pylayout.tcss

    LayoutApp Layout Is Vertical LayoutIsHorizontal

    from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass LayoutApp(App):\n    CSS_PATH = \"layout.tcss\"\n\n    def compose(self):\n        yield Container(\n            Label(\"Layout\"),\n            Label(\"Is\"),\n            Label(\"Vertical\"),\n            id=\"vertical-layout\",\n        )\n        yield Container(\n            Label(\"Layout\"),\n            Label(\"Is\"),\n            Label(\"Horizontal\"),\n            id=\"horizontal-layout\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    #vertical-layout {\n    layout: vertical;\n    background: darkmagenta;\n    height: auto;\n}\n\n#horizontal-layout {\n    layout: horizontal;\n    background: darkcyan;\n    height: auto;\n}\n\nLabel {\n    margin: 1;\n    width: 12;\n    color: black;\n    background: yellowgreen;\n}\n
    "},{"location":"styles/layout/#css","title":"CSS","text":"
    layout: horizontal;\n
    "},{"location":"styles/layout/#python","title":"Python","text":"
    widget.styles.layout = \"horizontal\"\n
    "},{"location":"styles/layout/#see-also","title":"See also","text":"
    • Layout guide.
    • Grid reference.
    "},{"location":"styles/margin/","title":"Margin","text":"

    The margin style specifies spacing around a widget.

    "},{"location":"styles/margin/#syntax","title":"Syntax","text":"
    \nmargin: <integer>\n      # one value for all edges\n      | <integer> <integer>\n      # top/bot   left/right\n      | <integer> <integer> <integer> <integer>;\n      # top       right     bot       left\n\nmargin-top: <integer>;\nmargin-right: <integer>;\nmargin-bottom: <integer>;\nmargin-left: <integer>;\n

    The margin specifies spacing around the four edges of the widget equal to the <integer> specified. The number of values given defines what edges get what margin:

    • 1 <integer> sets the same margin for the four edges of the widget;
    • 2 <integer> set margin for top/bottom and left/right edges, respectively.
    • 4 <integer> set margin for the top, right, bottom, and left edges, respectively.

    Tip

    To remember the order of the edges affected by the rule margin when it has 4 values, think of a clock. Its hand starts at the top and the goes clockwise: top, right, bottom, left.

    Alternatively, margin can be set for each edge individually through the styles margin-top, margin-right, margin-bottom, and margin-left, respectively.

    "},{"location":"styles/margin/#examples","title":"Examples","text":""},{"location":"styles/margin/#basic-usage","title":"Basic usage","text":"

    In the example below we add a large margin to a label, which makes it move away from the top-left corner of the screen.

    Outputmargin.pymargin.tcss

    MarginApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a \u258eits\u00a0path.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u258eremain.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass MarginApp(App):\n    CSS_PATH = \"margin.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = MarginApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n}\n\nLabel {\n    margin: 4 8;\n    background: blue 20%;\n    border: blue wide;\n    width: 100%;\n}\n
    "},{"location":"styles/margin/#all-margin-settings","title":"All margin settings","text":"

    The next example shows a grid. In each cell, we have a placeholder that has its margins set in different ways.

    Outputmargin_all.pymargin_all.tcss

    MarginAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin\u2502\u2502margin:\u00a01\u00a0\u2502 \u2502no\u00a0margin\u2502\u2502margin:\u00a01\u2502\u2502:\u00a01\u00a05\u2502\u25021\u00a02\u00a06\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin-bottom:\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502margin-right:\u00a0\u2502\u2502\u2502\u2502margin-left:\u00a03\u2502 \u2502\u2502\u25023\u2502\u2502\u2502\u2502\u2502 \u2502margin-top:\u00a04\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\n\n\nclass MarginAllApp(App):\n    CSS_PATH = \"margin_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Container(Placeholder(\"no margin\", id=\"p1\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1\", id=\"p2\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1 5\", id=\"p3\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1 1 2 6\", id=\"p4\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-top: 4\", id=\"p5\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-right: 3\", id=\"p6\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-bottom: 4\", id=\"p7\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-left: 3\", id=\"p8\"), classes=\"bordered\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MarginAllApp()\n    app.run()\n
    Screen {\n    background: $background;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    width: 100%;\n    height: 100%;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n}\n\n.bordered {\n    border: white round;\n}\n\n#p1 {\n    /* default is no margin */\n}\n\n#p2 {\n    margin: 1;\n}\n\n#p3 {\n    margin: 1 5;\n}\n\n#p4 {\n    margin: 1 1 2 6;\n}\n\n#p5 {\n    margin-top: 4;\n}\n\n#p6 {\n    margin-right: 3;\n}\n\n#p7 {\n    margin-bottom: 4;\n}\n\n#p8 {\n    margin-left: 3;\n}\n
    "},{"location":"styles/margin/#css","title":"CSS","text":"
    /* Set margin of 1 around all edges */\nmargin: 1;\n/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */\nmargin: 2 4;\n/* Set margin of 1 on the top, 2 on the right,\n                 3 on the bottom, and 4 on the left */\nmargin: 1 2 3 4;\n\nmargin-top: 1;\nmargin-right: 2;\nmargin-bottom: 3;\nmargin-left: 4;\n
    "},{"location":"styles/margin/#python","title":"Python","text":"

    Python does not provide the properties margin-top, margin-right, margin-bottom, and margin-left. However, you can set the margin to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

    # Set margin of 1 around all edges\nwidget.styles.margin = 1\n# Set margin of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.margin = (2, 4)\n# Set margin of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.margin = (1, 2, 3, 4)\n
    "},{"location":"styles/margin/#see-also","title":"See also","text":"
    • padding to add spacing around the content of a widget.
    "},{"location":"styles/max_height/","title":"Max-height","text":"

    The max-height style sets a maximum height for a widget.

    "},{"location":"styles/max_height/#syntax","title":"Syntax","text":"
    \nmax-height: <scalar>;\n

    The max-height style accepts a <scalar> that defines an upper bound for the height of a widget. That is, the height of a widget is never allowed to exceed max-height.

    "},{"location":"styles/max_height/#example","title":"Example","text":"

    The example below shows some placeholders that were defined to span vertically from the top edge of the terminal to the bottom edge. Then, we set max-height individually on each placeholder.

    Outputmax_height.pymax_height.tcss

    MaxHeightApp max-height:\u00a010w max-height:\u00a010 max-height:\u00a050% max-height:\u00a0999

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass MaxHeightApp(App):\n    CSS_PATH = \"max_height.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(\"max-height: 10w\", id=\"p1\"),\n            Placeholder(\"max-height: 999\", id=\"p2\"),\n            Placeholder(\"max-height: 50%\", id=\"p3\"),\n            Placeholder(\"max-height: 10\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MaxHeightApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n}\n\nPlaceholder {\n    height: 100%;\n    width: 1fr;\n}\n\n#p1 {\n    max-height: 10w;\n}\n\n#p2 {\n    max-height: 999;  /* (1)! */\n}\n\n#p3 {\n    max-height: 50%;\n}\n\n#p4 {\n    max-height: 10;\n}\n
    1. This won't affect the placeholder because its height is less than the maximum height.
    "},{"location":"styles/max_height/#css","title":"CSS","text":"
    /* Set the maximum height to 10 rows */\nmax-height: 10;\n\n/* Set the maximum height to 25% of the viewport height */\nmax-height: 25vh;\n
    "},{"location":"styles/max_height/#python","title":"Python","text":"
    # Set the maximum height to 10 rows\nwidget.styles.max_height = 10\n\n# Set the maximum height to 25% of the viewport height\nwidget.styles.max_height = \"25vh\"\n
    "},{"location":"styles/max_height/#see-also","title":"See also","text":"
    • min-height to set a lower bound on the height of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/max_width/","title":"Max-width","text":"

    The max-width style sets a maximum width for a widget.

    "},{"location":"styles/max_width/#syntax","title":"Syntax","text":"
    \nmax-width: <scalar>;\n

    The max-width style accepts a <scalar> that defines an upper bound for the width of a widget. That is, the width of a widget is never allowed to exceed max-width.

    "},{"location":"styles/max_width/#example","title":"Example","text":"

    The example below shows some placeholders that were defined to span horizontally from the left edge of the terminal to the right edge. Then, we set max-width individually on each placeholder.

    Outputmax_width.pymax_width.tcss

    MaxWidthApp max-width:\u00a0 50h max-width:\u00a0999 max-width:\u00a050% max-width:\u00a030

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass MaxWidthApp(App):\n    CSS_PATH = \"max_width.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(\"max-width: 50h\", id=\"p1\"),\n            Placeholder(\"max-width: 999\", id=\"p2\"),\n            Placeholder(\"max-width: 50%\", id=\"p3\"),\n            Placeholder(\"max-width: 30\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MaxWidthApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n}\n\nPlaceholder {\n    width: 100%;\n    height: 1fr;\n}\n\n#p1 {\n    max-width: 50h;\n}\n\n#p2 {\n    max-width: 999;  /* (1)! */\n}\n\n#p3 {\n    max-width: 50%;\n}\n\n#p4 {\n    max-width: 30;\n}\n
    1. This won't affect the placeholder because its width is less than the maximum width.
    "},{"location":"styles/max_width/#css","title":"CSS","text":"
    /* Set the maximum width to 10 rows */\nmax-width: 10;\n\n/* Set the maximum width to 25% of the viewport width */\nmax-width: 25vw;\n
    "},{"location":"styles/max_width/#python","title":"Python","text":"
    # Set the maximum width to 10 rows\nwidget.styles.max_width = 10\n\n# Set the maximum width to 25% of the viewport width\nwidget.styles.max_width = \"25vw\"\n
    "},{"location":"styles/max_width/#see-also","title":"See also","text":"
    • min-width to set a lower bound on the width of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/min_height/","title":"Min-height","text":"

    The min-height style sets a minimum height for a widget.

    "},{"location":"styles/min_height/#syntax","title":"Syntax","text":"
    \nmin-height: <scalar>;\n

    The min-height style accepts a <scalar> that defines a lower bound for the height of a widget. That is, the height of a widget is never allowed to be under min-height.

    "},{"location":"styles/min_height/#example","title":"Example","text":"

    The example below shows some placeholders with their height set to 50%. Then, we set min-height individually on each placeholder.

    Outputmin_height.pymin_height.tcss

    MinHeightApp min-height:\u00a025% min-height:\u00a075% min-height:\u00a030 min-height:\u00a040w \u2583\u2583

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass MinHeightApp(App):\n    CSS_PATH = \"min_height.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(\"min-height: 25%\", id=\"p1\"),\n            Placeholder(\"min-height: 75%\", id=\"p2\"),\n            Placeholder(\"min-height: 30\", id=\"p3\"),\n            Placeholder(\"min-height: 40w\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MinHeightApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n    overflow-y: auto;\n}\n\nPlaceholder {\n    width: 1fr;\n    height: 50%;\n}\n\n#p1 {\n    min-height: 25%;  /* (1)! */\n}\n\n#p2 {\n    min-height: 75%;\n}\n\n#p3 {\n    min-height: 30;\n}\n\n#p4 {\n    min-height: 40w;\n}\n
    1. This won't affect the placeholder because its height is larger than the minimum height.
    "},{"location":"styles/min_height/#css","title":"CSS","text":"
    /* Set the minimum height to 10 rows */\nmin-height: 10;\n\n/* Set the minimum height to 25% of the viewport height */\nmin-height: 25vh;\n
    "},{"location":"styles/min_height/#python","title":"Python","text":"
    # Set the minimum height to 10 rows\nwidget.styles.min_height = 10\n\n# Set the minimum height to 25% of the viewport height\nwidget.styles.min_height = \"25vh\"\n
    "},{"location":"styles/min_height/#see-also","title":"See also","text":"
    • max-height to set an upper bound on the height of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/min_width/","title":"Min-width","text":"

    The min-width style sets a minimum width for a widget.

    "},{"location":"styles/min_width/#syntax","title":"Syntax","text":"
    \nmin-width: <scalar>;\n

    The min-width style accepts a <scalar> that defines a lower bound for the width of a widget. That is, the width of a widget is never allowed to be under min-width.

    "},{"location":"styles/min_width/#example","title":"Example","text":"

    The example below shows some placeholders with their width set to 50%. Then, we set min-width individually on each placeholder.

    Outputmin_width.pymin_width.tcss

    MinWidthApp min-width:\u00a025% min-width:\u00a075% min-width:\u00a0100 min-width:\u00a0400h

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass MinWidthApp(App):\n    CSS_PATH = \"min_width.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(\"min-width: 25%\", id=\"p1\"),\n            Placeholder(\"min-width: 75%\", id=\"p2\"),\n            Placeholder(\"min-width: 100\", id=\"p3\"),\n            Placeholder(\"min-width: 400h\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MinWidthApp()\n    app.run()\n
    VerticalScroll {\n    height: 100%;\n    width: 100%;\n    overflow-x: auto;\n}\n\nPlaceholder {\n    height: 1fr;\n    width: 50%;\n}\n\n#p1 {\n    min-width: 25%;\n    /* (1)! */\n}\n\n#p2 {\n    min-width: 75%;\n}\n\n#p3 {\n    min-width: 100;\n}\n\n#p4 {\n    min-width: 400h;\n}\n
    1. This won't affect the placeholder because its width is larger than the minimum width.
    "},{"location":"styles/min_width/#css","title":"CSS","text":"
    /* Set the minimum width to 10 rows */\nmin-width: 10;\n\n/* Set the minimum width to 25% of the viewport width */\nmin-width: 25vw;\n
    "},{"location":"styles/min_width/#python","title":"Python","text":"
    # Set the minimum width to 10 rows\nwidget.styles.min_width = 10\n\n# Set the minimum width to 25% of the viewport width\nwidget.styles.min_width = \"25vw\"\n
    "},{"location":"styles/min_width/#see-also","title":"See also","text":"
    • max-width to set an upper bound on the width of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/offset/","title":"Offset","text":"

    The offset style defines an offset for the position of the widget.

    "},{"location":"styles/offset/#syntax","title":"Syntax","text":"
    \noffset: <scalar> <scalar>;\n\noffset-x: <scalar>;\noffset-y: <scalar>\n

    The two <scalar> in the offset define, respectively, the offsets in the horizontal and vertical axes for the widget.

    To specify an offset along a single axis, you can use offset-x and offset-y.

    "},{"location":"styles/offset/#example","title":"Example","text":"

    In this example, we have 3 widgets with differing offsets.

    Outputoffset.pyoffset.tcss

    OffsetApp \u258c\u2590 \u258cChani\u00a0(offset\u00a00\u00a0\u2590 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u258c-3)\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258cPaul\u00a0(offset\u00a08\u00a02)\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u258c\u2590 \u258c\u2590 \u258c\u2590 \u258c\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u258c\u2590 \u258c\u2590 \u258c\u2590 \u258cDuncan\u00a0(offset\u00a04\u00a0\u2590 \u258c10)\u2590 \u258c\u2590 \u258c\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass OffsetApp(App):\n    CSS_PATH = \"offset.tcss\"\n\n    def compose(self):\n        yield Label(\"Paul (offset 8 2)\", classes=\"paul\")\n        yield Label(\"Duncan (offset 4 10)\", classes=\"duncan\")\n        yield Label(\"Chani (offset 0 -3)\", classes=\"chani\")\n\n\nif __name__ == \"__main__\":\n    app = OffsetApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n    layout: horizontal;\n}\nLabel {\n    width: 20;\n    height: 10;\n    content-align: center middle;\n}\n\n.paul {\n    offset: 8 2;\n    background: red 20%;\n    border: outer red;\n    color: red;\n}\n\n.duncan {\n    offset: 4 10;\n    background: green 20%;\n    border: outer green;\n    color: green;\n}\n\n.chani {\n    offset: 0 -3;\n    background: blue 20%;\n    border: outer blue;\n    color: blue;\n}\n
    "},{"location":"styles/offset/#css","title":"CSS","text":"
    /* Move the widget 8 cells in the x direction and 2 in the y direction */\noffset: 8 2;\n\n/* Move the widget 4 cells in the x direction\noffset-x: 4;\n/* Move the widget -3 cells in the y direction\noffset-y: -3;\n
    "},{"location":"styles/offset/#python","title":"Python","text":"

    You cannot change programmatically the offset for a single axis. You have to set the two axes at the same time.

    # Move the widget 2 cells in the x direction, and 4 in the y direction.\nwidget.styles.offset = (2, 4)\n
    "},{"location":"styles/offset/#see-also","title":"See also","text":"
    • The layout guide section on offsets.
    "},{"location":"styles/opacity/","title":"Opacity","text":"

    The opacity style sets the opacity of a widget.

    While terminals are not capable of true opacity, Textual can create an approximation by blending widgets with their background color.

    "},{"location":"styles/opacity/#syntax","title":"Syntax","text":"
    \nopacity: <number> | <percentage>;\n

    The opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then opacity should be a value between 0 and 1, where 0 is the background color and 1 is fully opaque. If given as a percentage, 0% is the background color and 100% is fully opaque.

    Typically, if you set this value it would be somewhere between the two extremes. For instance, setting the opacity of a widget to 70% will make it appear dimmer than surrounding widgets, which could be used to display a disabled state.

    "},{"location":"styles/opacity/#example","title":"Example","text":"

    This example shows, from top to bottom, increasing opacity values for a label with a border and some text. When the opacity is zero, all we see is the (black) background.

    Outputopacity.pyopacity.tcss

    OpacityApp \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258copacity:\u00a00%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a025%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a050%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a075%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a0100%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass OpacityApp(App):\n    CSS_PATH = \"opacity.tcss\"\n\n    def compose(self):\n        yield Label(\"opacity: 0%\", id=\"zero-opacity\")\n        yield Label(\"opacity: 25%\", id=\"quarter-opacity\")\n        yield Label(\"opacity: 50%\", id=\"half-opacity\")\n        yield Label(\"opacity: 75%\", id=\"three-quarter-opacity\")\n        yield Label(\"opacity: 100%\", id=\"full-opacity\")\n\n\nif __name__ == \"__main__\":\n    app = OpacityApp()\n    app.run()\n
    #zero-opacity {\n    opacity: 0%;\n}\n\n#quarter-opacity {\n    opacity: 25%;\n}\n\n#half-opacity {\n    opacity: 50%;\n}\n\n#three-quarter-opacity {\n    opacity: 75%;\n}\n\n#full-opacity {\n    opacity: 100%;\n}\n\nScreen {\n    background: black;\n}\n\nLabel {\n    width: 100%;\n    height: 1fr;\n    border: outer dodgerblue;\n    background: lightseagreen;\n    content-align: center middle;\n    text-style: bold;\n}\n
    "},{"location":"styles/opacity/#css","title":"CSS","text":"
    /* Fade the widget to 50% against its parent's background */\nopacity: 50%;\n
    "},{"location":"styles/opacity/#python","title":"Python","text":"
    # Fade the widget to 50% against its parent's background\nwidget.styles.opacity = \"50%\"\n
    "},{"location":"styles/opacity/#see-also","title":"See also","text":"
    • text-opacity to blend the color of a widget's content with its background color.
    "},{"location":"styles/outline/","title":"Outline","text":"

    The outline style enables the drawing of a box around the content of a widget, which means the outline is drawn over the content area.

    Note

    border and outline cannot coexist in the same edge of a widget.

    "},{"location":"styles/outline/#syntax","title":"Syntax","text":"
    \noutline: [<border>] [<color>];\n\noutline-top: [<border>] [<color>];\noutline-right: [<border>] [<color>];\noutline-bottom: [<border>] [<color>];\noutline-left: [<border>] [<color>];\n

    The style outline accepts an optional <border> that sets the visual style of the widget outline and an optional <color> to set the color of the outline.

    Unlike the style border, the frame of the outline is drawn over the content area of the widget. This rule can be useful to add temporary emphasis on the content of a widget, if you want to draw the user's attention to it.

    "},{"location":"styles/outline/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

    textual borders\n
    "},{"location":"styles/outline/#examples","title":"Examples","text":""},{"location":"styles/outline/#basic-usage","title":"Basic usage","text":"

    This example shows a widget with an outline. Note how the outline occludes the text area.

    Outputoutline.pyoutline.tcss

    OutlineApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258e\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258e\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258end\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u258a \u258eath.\u258a \u258ehere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineApp(App):\n    CSS_PATH = \"outline.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = OutlineApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n}\n\nLabel {\n    margin: 4 8;\n    background: green 20%;\n    outline: wide green;\n    width: 100%;\n}\n
    "},{"location":"styles/outline/#all-outline-types","title":"All outline types","text":"

    The next example shows a grid with all the available outline types.

    Outputoutline_all.pyoutline_all.tcss

    AllOutlinesApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596 hkey\u2590inner\u258cnone \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u258couter\u2590\u2502round\u2502\u2502solid\u2502 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258atall\u258e\u258fvkey\u2595\u258ewide\u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass AllOutlinesApp(App):\n    CSS_PATH = \"outline_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"ascii\", id=\"ascii\"),\n            Label(\"blank\", id=\"blank\"),\n            Label(\"dashed\", id=\"dashed\"),\n            Label(\"double\", id=\"double\"),\n            Label(\"heavy\", id=\"heavy\"),\n            Label(\"hidden/none\", id=\"hidden\"),\n            Label(\"hkey\", id=\"hkey\"),\n            Label(\"inner\", id=\"inner\"),\n            Label(\"none\", id=\"none\"),\n            Label(\"outer\", id=\"outer\"),\n            Label(\"round\", id=\"round\"),\n            Label(\"solid\", id=\"solid\"),\n            Label(\"tall\", id=\"tall\"),\n            Label(\"vkey\", id=\"vkey\"),\n            Label(\"wide\", id=\"wide\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllOutlinesApp()\n    app.run()\n
    #ascii {\n    outline: ascii $accent;\n}\n\n#blank {\n    outline: blank $accent;\n}\n\n#dashed {\n    outline: dashed $accent;\n}\n\n#double {\n    outline: double $accent;\n}\n\n#heavy {\n    outline: heavy $accent;\n}\n\n#hidden {\n    outline: hidden $accent;\n}\n\n#hkey {\n    outline: hkey $accent;\n}\n\n#inner {\n    outline: inner $accent;\n}\n\n#none {\n    outline: none $accent;\n}\n\n#outer {\n    outline: outer $accent;\n}\n\n#round {\n    outline: round $accent;\n}\n\n#solid {\n    outline: solid $accent;\n}\n\n#tall {\n    outline: tall $accent;\n}\n\n#vkey {\n    outline: vkey $accent;\n}\n\n#wide {\n    outline: wide $accent;\n}\n\nGrid {\n    grid-size: 3 5;\n    align: center middle;\n    grid-gutter: 1 2;\n}\n\nLabel {\n    width: 20;\n    height: 3;\n    content-align: center middle;\n}\n
    "},{"location":"styles/outline/#borders-and-outlines","title":"Borders and outlines","text":"

    The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

    This example also shows that a widget cannot contain both a border and an outline:

    Outputoutline_vs_border.pyoutline_vs_border.tcss

    OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineBorderApp(App):\n    CSS_PATH = \"outline_vs_border.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, classes=\"outline\")\n        yield Label(TEXT, classes=\"border\")\n        yield Label(TEXT, classes=\"outline border\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineBorderApp()\n    app.run()\n
    Label {\n    height: 8;\n}\n\n.outline {\n    outline: $error round;\n}\n\n.border {\n    border: $success heavy;\n}\n
    "},{"location":"styles/outline/#css","title":"CSS","text":"
    /* Set a heavy white outline */\noutline:heavy white;\n\n/* set a red outline on the left */\noutline-left:outer red;\n
    "},{"location":"styles/outline/#python","title":"Python","text":"
    # Set a heavy white outline\nwidget.outline = (\"heavy\", \"white\")\n\n# Set a red outline on the left\nwidget.outline_left = (\"outer\", \"red\")\n
    "},{"location":"styles/outline/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    "},{"location":"styles/overflow/","title":"Overflow","text":"

    The overflow style specifies if and when scrollbars should be displayed.

    "},{"location":"styles/overflow/#syntax","title":"Syntax","text":"
    \noverflow: <overflow> <overflow>;\n\noverflow-x: <overflow>;\noverflow-y: <overflow>;\n

    The style overflow accepts two values that determine when to display scrollbars in a container widget. The two values set the overflow for the horizontal and vertical axes, respectively.

    Overflow may also be set individually for each axis:

    • overflow-x sets the overflow for the horizontal axis; and
    • overflow-y sets the overflow for the vertical axis.
    "},{"location":"styles/overflow/#defaults","title":"Defaults","text":"

    The default setting for containers is overflow: auto auto.

    Warning

    Some built-in containers like Horizontal and VerticalScroll override these defaults.

    "},{"location":"styles/overflow/#example","title":"Example","text":"

    Here we split the screen into left and right sections, each with three vertically scrolling widgets that do not fit into the height of the terminal.

    The left side has overflow-y: auto (the default) and will automatically show a scrollbar. The right side has overflow-y: hidden which will prevent a scrollbar from being shown.

    Outputoverflow.pyoverflow.tcss

    OverflowApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eand\u00a0through\u00a0me.\u258a\u258eand\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0\u258a\u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0\u258a \u258ewill\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a\u258eturn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u258a \u258eits\u00a0path.\u258a\u2581\u2581\u258epath.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0\u258a\u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u258a \u258ewill\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a\u258ebe\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u258a \u258eremain.\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eand\u00a0through\u00a0me.\u258a

    from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OverflowApp(App):\n    CSS_PATH = \"overflow.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"left\"),\n            VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"right\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = OverflowApp()\n    app.run()\n
    Screen {\n    background: $background;\n    color: black;\n}\n\nVerticalScroll {\n    width: 1fr;\n}\n\nStatic {\n    margin: 1 2;\n    background: green 80%;\n    border: green wide;\n    color: white 90%;\n    height: auto;\n}\n\n#right {\n    overflow-y: hidden;\n}\n
    "},{"location":"styles/overflow/#css","title":"CSS","text":"
    /* Automatic scrollbars on both axes (the default) */\noverflow: auto auto;\n\n/* Hide the vertical scrollbar */\noverflow-y: hidden;\n\n/* Always show the horizontal scrollbar */\noverflow-x: scroll;\n
    "},{"location":"styles/overflow/#python","title":"Python","text":"

    Overflow cannot be programmatically set for both axes at the same time.

    # Hide the vertical scrollbar\nwidget.styles.overflow_y = \"hidden\"\n\n# Always show the horizontal scrollbar\nwidget.styles.overflow_x = \"scroll\"\n
    "},{"location":"styles/padding/","title":"Padding","text":"

    The padding style specifies spacing around the content of a widget.

    "},{"location":"styles/padding/#syntax","title":"Syntax","text":"
    \npadding: <integer> # one value for all edges\n       | <integer> <integer>\n       # top/bot   left/right\n       | <integer> <integer> <integer> <integer>;\n       # top       right     bot       left\n\npadding-top: <integer>;\npadding-right: <integer>;\npadding-bottom: <integer>;\npadding-left: <integer>;\n

    The padding specifies spacing around the content of a widget, thus this spacing is added inside the widget. The values of the <integer> determine how much spacing is added and the number of values define what edges get what padding:

    • 1 <integer> sets the same padding for the four edges of the widget;
    • 2 <integer> set padding for top/bottom and left/right edges, respectively.
    • 4 <integer> set padding for the top, right, bottom, and left edges, respectively.

    Tip

    To remember the order of the edges affected by the rule padding when it has 4 values, think of a clock. Its hand starts at the top and then goes clockwise: top, right, bottom, left.

    Alternatively, padding can be set for each edge individually through the rules padding-top, padding-right, padding-bottom, and padding-left, respectively.

    "},{"location":"styles/padding/#example","title":"Example","text":""},{"location":"styles/padding/#basic-usage","title":"Basic usage","text":"

    This example adds padding around some text.

    Outputpadding.pypadding.tcss

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    CSS_PATH = \"padding.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: blue;\n}\n\nLabel {\n    padding: 4 8;\n    background: blue 20%;\n    width: 100%;\n}\n
    "},{"location":"styles/padding/#all-padding-settings","title":"All padding settings","text":"

    The next example shows a grid. In each cell, we have a placeholder that has its padding set in different ways. The effect of each padding setting is noticeable in the colored background around the text of each placeholder.

    Outputpadding_all.pypadding_all.tcss

    PaddingAllApp no\u00a0padding padding:\u00a01padding:padding:\u00a01\u00a01 1\u00a052\u00a06 padding-right:\u00a03padding-bottom:\u00a04padding-left:\u00a03 padding-top:\u00a04

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass PaddingAllApp(App):\n    CSS_PATH = \"padding_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(\"no padding\", id=\"p1\"),\n            Placeholder(\"padding: 1\", id=\"p2\"),\n            Placeholder(\"padding: 1 5\", id=\"p3\"),\n            Placeholder(\"padding: 1 1 2 6\", id=\"p4\"),\n            Placeholder(\"padding-top: 4\", id=\"p5\"),\n            Placeholder(\"padding-right: 3\", id=\"p6\"),\n            Placeholder(\"padding-bottom: 4\", id=\"p7\"),\n            Placeholder(\"padding-left: 3\", id=\"p8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = PaddingAllApp()\n    app.run()\n
    Screen {\n    background: $background;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    width: auto;\n    height: auto;\n}\n\n#p1 {\n    /* default is no padding */\n}\n\n#p2 {\n    padding: 1;\n}\n\n#p3 {\n    padding: 1 5;\n}\n\n#p4 {\n    padding: 1 1 2 6;\n}\n\n#p5 {\n    padding-top: 4;\n}\n\n#p6 {\n    padding-right: 3;\n}\n\n#p7 {\n    padding-bottom: 4;\n}\n\n#p8 {\n    padding-left: 3;\n}\n
    "},{"location":"styles/padding/#css","title":"CSS","text":"
    /* Set padding of 1 around all edges */\npadding: 1;\n/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */\npadding: 2 4;\n/* Set padding of 1 on the top, 2 on the right,\n                 3 on the bottom, and 4 on the left */\npadding: 1 2 3 4;\n\npadding-top: 1;\npadding-right: 2;\npadding-bottom: 3;\npadding-left: 4;\n
    "},{"location":"styles/padding/#python","title":"Python","text":"

    In Python, you cannot set any of the individual padding styles padding-top, padding-right, padding-bottom, and padding-left.

    However, you can set padding to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

    # Set padding of 1 around all edges\nwidget.styles.padding = 1\n# Set padding of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.padding = (2, 4)\n# Set padding of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.padding = (1, 2, 3, 4)\n
    "},{"location":"styles/padding/#see-also","title":"See also","text":"
    • box-sizing to specify how to account for padding in a widget's dimensions.
    • padding to add spacing around a widget.
    "},{"location":"styles/scrollbar_gutter/","title":"Scrollbar-gutter","text":"

    The scrollbar-gutter style allows reserving space for a vertical scrollbar.

    "},{"location":"styles/scrollbar_gutter/#syntax","title":"Syntax","text":"
    \nscrollbar-gutter: auto | stable;\n
    "},{"location":"styles/scrollbar_gutter/#values","title":"Values","text":"Value Description auto (default) No space is reserved for a vertical scrollbar. stable Space is reserved for a vertical scrollbar.

    Setting the value to stable prevents unwanted layout changes when the scrollbar becomes visible, whereas the default value of auto means that the layout of your application is recomputed when a vertical scrollbar becomes needed.

    "},{"location":"styles/scrollbar_gutter/#example","title":"Example","text":"

    In the example below, notice the gap reserved for the scrollbar on the right side of the terminal window.

    Outputscrollbar_gutter.pyscrollbar_gutter.tcss

    ScrollbarGutterApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    from textual.app import App\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass ScrollbarGutterApp(App):\n    CSS_PATH = \"scrollbar_gutter.tcss\"\n\n    def compose(self):\n        yield Static(TEXT, id=\"text-box\")\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarGutterApp()\n    app.run()\n
    Screen {\n    scrollbar-gutter: stable;\n}\n\n#text-box {\n    color: floralwhite;\n    background: darkmagenta;\n}\n
    "},{"location":"styles/scrollbar_gutter/#css","title":"CSS","text":"
    scrollbar-gutter: auto;    /* Don't reserve space for a vertical scrollbar. */\nscrollbar-gutter: stable;  /* Reserve space for a vertical scrollbar. */\n
    "},{"location":"styles/scrollbar_gutter/#python","title":"Python","text":"
    self.styles.scrollbar_gutter = \"auto\"    # Don't reserve space for a vertical scrollbar.\nself.styles.scrollbar_gutter = \"stable\"  # Reserve space for a vertical scrollbar.\n
    "},{"location":"styles/scrollbar_size/","title":"Scrollbar-size","text":"

    The scrollbar-size style defines the width of the scrollbars.

    "},{"location":"styles/scrollbar_size/#syntax","title":"Syntax","text":"
    \nscrollbar-size: <integer> <integer>;\n              # horizontal vertical\n\nscrollbar-size-horizontal: <integer>;\nscrollbar-size-vertical: <integer>;\n

    The scrollbar-size style takes two <integer> to set the horizontal and vertical scrollbar sizes, respectively. This customisable size is the width of the scrollbar, given that its length will always be 100% of the container.

    The scrollbar widths may also be set individually with scrollbar-size-horizontal and scrollbar-size-vertical.

    "},{"location":"styles/scrollbar_size/#examples","title":"Examples","text":""},{"location":"styles/scrollbar_size/#basic-usage","title":"Basic usage","text":"

    In this example we modify the size of the widget's scrollbar to be much larger than usual.

    Outputscrollbar_size.pyscrollbar_size.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2581\u2581\u2581\u2581 I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    from textual.app import App\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbar_size.tcss\"\n\n    def compose(self):\n        yield ScrollableContainer(Label(TEXT * 5), classes=\"panel\")\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: blue 80%;\n    layout: horizontal;\n}\n\nLabel {\n    padding: 1 2;\n    width: 200;\n}\n\n.panel {\n    scrollbar-size: 10 4;\n    padding: 1 2;\n}\n
    "},{"location":"styles/scrollbar_size/#scrollbar-sizes-comparison","title":"Scrollbar sizes comparison","text":"

    In the next example we show three containers with differently sized scrollbars.

    Tip

    If you want to hide the scrollbar but still allow the container to scroll using the mousewheel or keyboard, you can set the scrollbar size to 0.

    Outputscrollbar_size2.pyscrollbar_size2.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587\u2587 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.\u2582I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 \u258fAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u258f \u258fWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0th\u258f \u258fI\u00a0must\u00a0not\u00a0fear.\u258f \u258fFear\u00a0is\u00a0the\u00a0mind-killer.\u258f \u258f\u2589\u258f

    from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbar_size2.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            ScrollableContainer(Label(TEXT * 5), id=\"v1\"),\n            ScrollableContainer(Label(TEXT * 5), id=\"v2\"),\n            ScrollableContainer(Label(TEXT * 5), id=\"v3\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    ScrollableContainer {\n    width: 1fr;\n}\n\n#v1 {\n    scrollbar-size: 5 1;\n    background: red 20%;\n}\n\n#v2 {\n    scrollbar-size-vertical: 1;\n    background: green 20%;\n}\n\n#v3 {\n    scrollbar-size-horizontal: 5;\n    background: blue 20%;\n}\n
    "},{"location":"styles/scrollbar_size/#css","title":"CSS","text":"
    /* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */\nscrollbar-size: 10 4;\n\n/* Set horizontal scrollbar to 10 */\nscrollbar-size-horizontal: 10;\n\n/* Set vertical scrollbar to 4 */\nscrollbar-size-vertical: 4;\n
    "},{"location":"styles/scrollbar_size/#python","title":"Python","text":"

    The style scrollbar-size has no Python equivalent. The scrollbar sizes must be set independently:

    # Set horizontal scrollbar to 10:\nwidget.styles.scrollbar_size_horizontal = 10\n# Set vertical scrollbar to 4:\nwidget.styles.scrollbar_size_vertical = 4\n
    "},{"location":"styles/text_align/","title":"Text-align","text":"

    The text-align style sets the text alignment in a widget.

    "},{"location":"styles/text_align/#syntax","title":"Syntax","text":"
    \ntext-align: <text-align>;\n

    The text-align style accepts a value of the type <text-align> that defines how text is aligned inside the widget.

    "},{"location":"styles/text_align/#defaults","title":"Defaults","text":"

    The default value is start.

    "},{"location":"styles/text_align/#example","title":"Example","text":"

    This example shows, from top to bottom: left, center, right, and justify text alignments.

    Outputtext_align.pytext_align.tcss

    TextAlign Left\u00a0alignedCenter\u00a0aligned I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0 mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0 obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Right\u00a0alignedJustified \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0theI\u00a0\u00a0must\u00a0\u00a0not\u00a0\u00a0fear.\u00a0\u00a0Fear\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0themind-killer.\u00a0\u00a0\u00a0\u00a0\u00a0Fear\u00a0\u00a0\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0totallittle-death\u00a0\u00a0\u00a0that\u00a0\u00a0\u00a0brings\u00a0\u00a0\u00a0total obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I \u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0andwill\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u00a0me\u00a0\u00a0and \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.through\u00a0me.

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\nTEXT = (\n    \"I must not fear. Fear is the mind-killer. Fear is the little-death that \"\n    \"brings total obliteration. I will face my fear. I will permit it to pass over \"\n    \"me and through me.\"\n)\n\n\nclass TextAlign(App):\n    CSS_PATH = \"text_align.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"[b]Left aligned[/]\\n\" + TEXT, id=\"one\"),\n            Label(\"[b]Center aligned[/]\\n\" + TEXT, id=\"two\"),\n            Label(\"[b]Right aligned[/]\\n\" + TEXT, id=\"three\"),\n            Label(\"[b]Justified[/]\\n\" + TEXT, id=\"four\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = TextAlign()\n    app.run()\n
    #one {\n    text-align: left;\n    background: lightblue;\n}\n\n#two {\n    text-align: center;\n    background: indianred;\n}\n\n#three {\n    text-align: right;\n    background: palegreen;\n}\n\n#four {\n    text-align: justify;\n    background: palevioletred;\n}\n\nLabel {\n    padding: 1 2;\n    height: 100%;\n    color: auto;\n}\n\nGrid {\n    grid-size: 2 2;\n}\n
    "},{"location":"styles/text_align/#css","title":"CSS","text":"
    /* Set text in the widget to be right aligned */\ntext-align: right;\n
    "},{"location":"styles/text_align/#python","title":"Python","text":"
    # Set text in the widget to be right aligned\nwidget.styles.text_align = \"right\"\n
    "},{"location":"styles/text_align/#see-also","title":"See also","text":"
    • align to set the alignment of children widgets inside a container.
    • content-align to set the alignment of content inside a widget.
    "},{"location":"styles/text_opacity/","title":"Text-opacity","text":"

    The text-opacity style blends the foreground color (i.e. text) with the background color.

    "},{"location":"styles/text_opacity/#syntax","title":"Syntax","text":"
    \ntext-opacity: <number> | <percentage>;\n

    The text opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then text-opacity should be a value between 0 and 1, where 0 makes the foreground color match the background (effectively making text invisible) and 1 will display text as normal. If given as a percentage, 0% will result in invisible text, and 100% will display fully opaque text.

    Typically, if you set this value it would be somewhere between the two extremes. For instance, setting text-opacity to 70% would result in slightly faded text. Setting it to 0.3 would result in very dim text.

    Warning

    Be careful not to set text opacity so low as to make it hard to read.

    "},{"location":"styles/text_opacity/#example","title":"Example","text":"

    This example shows, from top to bottom, increasing text-opacity values.

    Outputtext_opacity.pytext_opacity.tcss

    TextOpacityApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a025%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a050%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a075%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a0100%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass TextOpacityApp(App):\n    CSS_PATH = \"text_opacity.tcss\"\n\n    def compose(self):\n        yield Label(\"text-opacity: 0%\", id=\"zero-opacity\")\n        yield Label(\"text-opacity: 25%\", id=\"quarter-opacity\")\n        yield Label(\"text-opacity: 50%\", id=\"half-opacity\")\n        yield Label(\"text-opacity: 75%\", id=\"three-quarter-opacity\")\n        yield Label(\"text-opacity: 100%\", id=\"full-opacity\")\n\n\nif __name__ == \"__main__\":\n    app = TextOpacityApp()\n    app.run()\n
    #zero-opacity {\n    text-opacity: 0%;\n}\n\n#quarter-opacity {\n    text-opacity: 25%;\n}\n\n#half-opacity {\n    text-opacity: 50%;\n}\n\n#three-quarter-opacity {\n    text-opacity: 75%;\n}\n\n#full-opacity {\n    text-opacity: 100%;\n}\n\nLabel {\n    height: 1fr;\n    width: 100%;\n    text-align: center;\n    text-style: bold;\n}\n
    "},{"location":"styles/text_opacity/#css","title":"CSS","text":"
    /* Set the text to be \"half-faded\" against the background of the widget */\ntext-opacity: 50%;\n
    "},{"location":"styles/text_opacity/#python","title":"Python","text":"
    # Set the text to be \"half-faded\" against the background of the widget\nwidget.styles.text_opacity = \"50%\"\n
    "},{"location":"styles/text_opacity/#see-also","title":"See also","text":"
    • opacity to specify the opacity of a whole widget.
    "},{"location":"styles/text_style/","title":"Text-style","text":"

    The text-style style sets the style for the text in a widget.

    "},{"location":"styles/text_style/#syntax","title":"Syntax","text":"
    \ntext-style: <text-style>;\n

    text-style will take all the values specified and will apply that styling combination to the text in the widget.

    "},{"location":"styles/text_style/#examples","title":"Examples","text":""},{"location":"styles/text_style/#basic-usage","title":"Basic usage","text":"

    Each of the three text panels has a different text style, respectively bold, italic, and reverse, from left to right.

    Outputtext_style.pytext_style.tcss

    TextStyleApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0 I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Onlythere\u00a0will\u00a0be\u00a0nothing.\u00a0Only Only\u00a0I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass TextStyleApp(App):\n    CSS_PATH = \"text_style.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, id=\"lbl1\")\n        yield Label(TEXT, id=\"lbl2\")\n        yield Label(TEXT, id=\"lbl3\")\n\n\nif __name__ == \"__main__\":\n    app = TextStyleApp()\n    app.run()\n
    Screen {\n    layout: horizontal;\n}\nLabel {\n    width: 1fr;\n}\n#lbl1 {\n    background: red 30%;\n    text-style: bold;\n}\n#lbl2 {\n    background: green 30%;\n    text-style: italic;\n}\n#lbl3 {\n    background: blue 30%;\n    text-style: reverse;\n}\n
    "},{"location":"styles/text_style/#all-text-styles","title":"All text styles","text":"

    The next example shows all different text styles on their own, as well as some combinations of styles in a single widget.

    Outputtext_style_all.pytext_style_all.tcss

    AllTextStyleApp nonebolditalicreverse I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. strikeunderlinebold\u00a0italicreverse\u00a0strike I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass AllTextStyleApp(App):\n    CSS_PATH = \"text_style_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"none\\n\" + TEXT, id=\"lbl1\"),\n            Label(\"bold\\n\" + TEXT, id=\"lbl2\"),\n            Label(\"italic\\n\" + TEXT, id=\"lbl3\"),\n            Label(\"reverse\\n\" + TEXT, id=\"lbl4\"),\n            Label(\"strike\\n\" + TEXT, id=\"lbl5\"),\n            Label(\"underline\\n\" + TEXT, id=\"lbl6\"),\n            Label(\"bold italic\\n\" + TEXT, id=\"lbl7\"),\n            Label(\"reverse strike\\n\" + TEXT, id=\"lbl8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllTextStyleApp()\n    app.run()\n
    #lbl1 {\n    text-style: none;\n}\n\n#lbl2 {\n    text-style: bold;\n}\n\n#lbl3 {\n    text-style: italic;\n}\n\n#lbl4 {\n    text-style: reverse;\n}\n\n#lbl5 {\n    text-style: strike;\n}\n\n#lbl6 {\n    text-style: underline;\n}\n\n#lbl7 {\n    text-style: bold italic;\n}\n\n#lbl8 {\n    text-style: reverse strike;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n    margin: 1 2;\n    height: 100%;\n}\n\nLabel {\n    height: 100%;\n}\n
    "},{"location":"styles/text_style/#css","title":"CSS","text":"
    text-style: italic;\n
    "},{"location":"styles/text_style/#python","title":"Python","text":"
    widget.styles.text_style = \"italic\"\n
    "},{"location":"styles/tint/","title":"Tint","text":"

    The tint style blends a color with the whole widget.

    "},{"location":"styles/tint/#syntax","title":"Syntax","text":"
    \ntint: <color> [<percentage>];\n

    The tint style blends a <color> with the widget. The color should likely have an alpha component (specified directly in the color used or by the optional <percentage>), otherwise the end result will obscure the widget content.

    "},{"location":"styles/tint/#example","title":"Example","text":"

    This examples shows a green tint with gradually increasing alpha.

    Outputtint.pytint.tcss

    TintApp tint:\u00a0green\u00a00%; tint:\u00a0green\u00a010%; tint:\u00a0green\u00a020%; tint:\u00a0green\u00a030%; tint:\u00a0green\u00a040%; tint:\u00a0green\u00a050%; \u2584\u2584 tint:\u00a0green\u00a060%; tint:\u00a0green\u00a070%;

    from textual.app import App\nfrom textual.color import Color\nfrom textual.widgets import Label\n\n\nclass TintApp(App):\n    CSS_PATH = \"tint.tcss\"\n\n    def compose(self):\n        color = Color.parse(\"green\")\n        for tint_alpha in range(0, 101, 10):\n            widget = Label(f\"tint: green {tint_alpha}%;\")\n            widget.styles.tint = color.with_alpha(tint_alpha / 100)  # (1)!\n            yield widget\n\n\nif __name__ == \"__main__\":\n    app = TintApp()\n    app.run()\n
    1. We set the tint to a Color instance with varying levels of opacity, set through the method with_alpha.
    Label {\n    height: 3;\n    width: 100%;\n    text-style: bold;\n    background: white;\n    color: black;\n    content-align: center middle;\n}\n
    "},{"location":"styles/tint/#css","title":"CSS","text":"
    /* A red tint (could indicate an error) */\ntint: red 20%;\n\n/* A green tint */\ntint: rgba(0, 200, 0, 0.3);\n
    "},{"location":"styles/tint/#python","title":"Python","text":"
    # A red tint\nfrom textual.color import Color\nwidget.styles.tint = Color.parse(\"red\").with_alpha(0.2);\n\n# A green tint\nwidget.styles.tint = \"rgba(0, 200, 0, 0.3)\"\n
    "},{"location":"styles/visibility/","title":"Visibility","text":"

    The visibility style determines whether a widget is visible or not.

    "},{"location":"styles/visibility/#syntax","title":"Syntax","text":"
    \nvisibility: hidden | visible;\n

    visibility takes one of two values to set the visibility of a widget.

    "},{"location":"styles/visibility/#values","title":"Values","text":"Value Description hidden The widget will be invisible. visible (default) The widget will be displayed as normal."},{"location":"styles/visibility/#visibility-inheritance","title":"Visibility inheritance","text":"

    Note

    Children of an invisible container can be visible.

    By default, children inherit the visibility of their parents. So, if a container is set to be invisible, its children widgets will also be invisible by default. However, those widgets can be made visible if their visibility is explicitly set to visibility: visible. This is shown in the second example below.

    "},{"location":"styles/visibility/#examples","title":"Examples","text":""},{"location":"styles/visibility/#basic-usage","title":"Basic usage","text":"

    Note that the second widget is hidden while leaving a space where it would have been rendered.

    Outputvisibility.pyvisibility.tcss

    VisibilityApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass VisibilityApp(App):\n    CSS_PATH = \"visibility.tcss\"\n\n    def compose(self):\n        yield Label(\"Widget 1\")\n        yield Label(\"Widget 2\", classes=\"invisible\")\n        yield Label(\"Widget 3\")\n\n\nif __name__ == \"__main__\":\n    app = VisibilityApp()\n    app.run()\n
    Screen {\n    background: green;\n}\n\nLabel {\n    height: 5;\n    width: 100%;\n    background: white;\n    color: blue;\n    border: heavy blue;\n}\n\nLabel.invisible {\n    visibility: hidden;\n}\n
    "},{"location":"styles/visibility/#overriding-container-visibility","title":"Overriding container visibility","text":"

    The next example shows the interaction of the visibility style with invisible containers that have visible children. The app below has three rows with a Horizontal container per row and three placeholders per row. The containers all have a white background, and then:

    • the top container is visible by default (we can see the white background around the placeholders);
    • the middle container is invisible and the children placeholders inherited that setting;
    • the bottom container is invisible but the children placeholders are visible because they were set to be visible.
    Outputvisibility_containers.pyvisibility_containers.tcss

    VisibilityContainersApp PlaceholderPlaceholderPlaceholder PlaceholderPlaceholderPlaceholder

    from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass VisibilityContainersApp(App):\n    CSS_PATH = \"visibility_containers.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"top\",\n            ),\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"middle\",\n            ),\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"bot\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = VisibilityContainersApp()\n    app.run()\n
    Horizontal {\n    padding: 1 2;     /* (1)! */\n    background: white;\n    height: 1fr;\n}\n\n#top {}               /* (2)! */\n\n#middle {             /* (3)! */\n    visibility: hidden;\n}\n\n#bot {                /* (4)! */\n    visibility: hidden;\n}\n\n#bot > Placeholder {  /* (5)! */\n    visibility: visible;\n}\n\nPlaceholder {\n    width: 1fr;\n}\n
    1. The padding and the white background let us know when the Horizontal is visible.
    2. The top Horizontal is visible by default, and so are its children.
    3. The middle Horizontal is made invisible and its children will inherit that setting.
    4. The bottom Horizontal is made invisible...
    5. ... but its children override that setting and become visible.
    "},{"location":"styles/visibility/#css","title":"CSS","text":"
    /* Widget is invisible */\nvisibility: hidden;\n\n/* Widget is visible */\nvisibility: visible;\n
    "},{"location":"styles/visibility/#python","title":"Python","text":"
    # Widget is invisible\nself.styles.visibility = \"hidden\"\n\n# Widget is visible\nself.styles.visibility = \"visible\"\n

    There is also a shortcut to set a Widget's visibility. The visible property on Widget may be set to True or False.

    # Make a widget invisible\nwidget.visible = False\n\n# Make the widget visible again\nwidget.visible = True\n
    "},{"location":"styles/visibility/#see-also","title":"See also","text":"
    • display to specify whether a widget is displayed or not.
    "},{"location":"styles/width/","title":"Width","text":"

    The width style sets a widget's width.

    "},{"location":"styles/width/#syntax","title":"Syntax","text":"
    \nwidth: <scalar>;\n

    The style width needs a <scalar> to determine the horizontal length of the width. By default, it sets the width of the content area, but if box-sizing is set to border-box it sets the width of the border area.

    "},{"location":"styles/width/#examples","title":"Examples","text":""},{"location":"styles/width/#basic-usage","title":"Basic usage","text":"

    This example adds a widget with 50% width of the screen.

    Outputwidth.pywidth.tcss

    WidthApp Widget

    from textual.app import App\nfrom textual.widget import Widget\n\n\nclass WidthApp(App):\n    CSS_PATH = \"width.tcss\"\n\n    def compose(self):\n        yield Widget()\n\n\nif __name__ == \"__main__\":\n    app = WidthApp()\n    app.run()\n
    Screen > Widget {\n    background: green;\n    width: 50%;\n    color: white;\n}\n
    "},{"location":"styles/width/#all-width-formats","title":"All width formats","text":"Outputwidth_comparison.pywidth_comparison.tcss

    WidthComparisonApp #cells#percent#w#h#vw#vh#auto#fr1#fr3 \u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Placeholder, Static\n\n\nclass Ruler(Static):\n    def compose(self):\n        ruler_text = \"\u00b7\u00b7\u00b7\u00b7\u2022\" * 100\n        yield Label(ruler_text)\n\n\nclass WidthComparisonApp(App):\n    CSS_PATH = \"width_comparison.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(id=\"cells\"),  # (1)!\n            Placeholder(id=\"percent\"),\n            Placeholder(id=\"w\"),\n            Placeholder(id=\"h\"),\n            Placeholder(id=\"vw\"),\n            Placeholder(id=\"vh\"),\n            Placeholder(id=\"auto\"),\n            Placeholder(id=\"fr1\"),\n            Placeholder(id=\"fr3\"),\n        )\n        yield Ruler()\n\n\nif __name__ == \"__main__\":\n    app = WidthComparisonApp()\n    app.run()\n
    1. The id of the placeholder identifies which unit will be used to set the width of the widget.
    #cells {\n    width: 9;      /* (1)! */\n}\n#percent {\n    width: 12.5%;  /* (2)! */\n}\n#w {\n    width: 10w;    /* (3)! */\n}\n#h {\n    width: 25h;    /* (4)! */\n}\n#vw {\n    width: 15vw;   /* (5)! */\n}\n#vh {\n    width: 25vh;   /* (6)! */\n}\n#auto {\n    width: auto;   /* (7)! */\n}\n#fr1 {\n    width: 1fr;    /* (8)! */\n}\n#fr3 {\n    width: 3fr;    /* (9)! */\n}\n\nScreen {\n    layers: ruler;\n}\n\nRuler {\n    layer: ruler;\n    dock: bottom;\n    overflow: hidden;\n    height: 1;\n    background: $accent;\n}\n
    1. This sets the width to 9 columns.
    2. This sets the width to 12.5% of the space made available by the container. The container is 80 columns wide, so 12.5% of 80 is 10.
    3. This sets the width to 10% of the width of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the width of the Horizontal is 80 and 10% of 80 is 8.
    4. This sets the width to 25% of the height of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the height of the Horizontal is 24 and 25% of 24 is 6.
    5. This sets the width to 15% of the viewport width, which is 80. 15% of 80 is 12.
    6. This sets the width to 25% of the viewport height, which is 24. 25% of 24 is 6.
    7. This sets the width of the placeholder to be the optimal size that fits the content without scrolling. Because the content is the string \"#auto\", the placeholder has its width set to 5.
    8. This sets the width to 1fr, which means this placeholder will have a third of the width of a placeholder with 3fr.
    9. This sets the width to 3fr, which means this placeholder will have triple the width of a placeholder with 1fr.
    "},{"location":"styles/width/#css","title":"CSS","text":"
    /* Explicit cell width */\nwidth: 10;\n\n/* Percentage width */\nwidth: 50%;\n\n/* Automatic width */\nwidth: auto;\n
    "},{"location":"styles/width/#python","title":"Python","text":"
    widget.styles.width = 10\nwidget.styles.width = \"50%\nwidget.styles.width = \"auto\"\n
    "},{"location":"styles/width/#see-also","title":"See also","text":"
    • max-width and min-width to limit the width of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/grid/","title":"Grid","text":"

    There are a number of styles relating to the Textual grid layout.

    For an in-depth look at the grid layout, visit the grid guide.

    Property Description column-span Number of columns a cell spans. grid-columns Width of grid columns. grid-gutter Spacing between grid cells. grid-rows Height of grid rows. grid-size Number of columns and rows in the grid layout. row-span Number of rows a cell spans."},{"location":"styles/grid/#syntax","title":"Syntax","text":"
    \ncolumn-span: <integer>;\n\ngrid-columns: <scalar>+;\n\ngrid-gutter: <scalar> [<scalar>];\n\ngrid-rows: <scalar>+;\n\ngrid-size: <integer> [<integer>];\n\nrow-span: <integer>;\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/grid/#example","title":"Example","text":"

    The example below shows all the styles above in action. The grid-size: 3 4; declaration sets the grid to 3 columns and 4 rows. The first cell of the grid, tinted magenta, shows a cell spanning multiple rows and columns. The spacing between grid cells is defined by the grid-gutter style.

    Outputgrid.pygrid.tcss

    GridApp Grid\u00a0cell\u00a01Grid\u00a0cell\u00a02 row-span:\u00a03; column-span:\u00a02; Grid\u00a0cell\u00a03 Grid\u00a0cell\u00a04 Grid\u00a0cell\u00a05Grid\u00a0cell\u00a06Grid\u00a0cell\u00a07

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass GridApp(App):\n    CSS_PATH = \"grid.tcss\"\n\n    def compose(self):\n        yield Static(\"Grid cell 1\\n\\nrow-span: 3;\\ncolumn-span: 2;\", id=\"static1\")\n        yield Static(\"Grid cell 2\", id=\"static2\")\n        yield Static(\"Grid cell 3\", id=\"static3\")\n        yield Static(\"Grid cell 4\", id=\"static4\")\n        yield Static(\"Grid cell 5\", id=\"static5\")\n        yield Static(\"Grid cell 6\", id=\"static6\")\n        yield Static(\"Grid cell 7\", id=\"static7\")\n\n\nif __name__ == \"__main__\":\n    app = GridApp()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3 4;\n    grid-rows: 1fr;\n    grid-columns: 1fr;\n    grid-gutter: 1;\n}\n\nStatic {\n    color: auto;\n    background: lightblue;\n    height: 100%;\n    padding: 1 2;\n}\n\n#static1 {\n    tint: magenta 40%;\n    row-span: 3;\n    column-span: 2;\n}\n

    Warning

    The styles listed on this page will only work when the layout is grid.

    "},{"location":"styles/grid/#see-also","title":"See also","text":"
    • The grid layout guide.
    "},{"location":"styles/grid/column_span/","title":"Column-span","text":"

    The column-span style specifies how many columns a widget will span in a grid layout.

    Note

    This style only affects widgets that are direct children of a widget with layout: grid.

    "},{"location":"styles/grid/column_span/#syntax","title":"Syntax","text":"
    \ncolumn-span: <integer>;\n

    The column-span style accepts a single non-negative <integer> that quantifies how many columns the given widget spans.

    "},{"location":"styles/grid/column_span/#example","title":"Example","text":"

    The example below shows a 4 by 4 grid where many placeholders span over several columns.

    Outputcolumn_span.pycolumn_span.tcss

    MyApp #p1 #p2#p3 #p4#p5 #p6#p7

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass MyApp(App):\n    CSS_PATH = \"column_span.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(id=\"p1\"),\n            Placeholder(id=\"p2\"),\n            Placeholder(id=\"p3\"),\n            Placeholder(id=\"p4\"),\n            Placeholder(id=\"p5\"),\n            Placeholder(id=\"p6\"),\n            Placeholder(id=\"p7\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    #p1 {\n    column-span: 4;\n}\n#p2 {\n    column-span: 3;\n}\n#p3 {\n    column-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p4 {\n    column-span: 2;\n}\n#p5 {\n    column-span: 2;\n}\n#p6 {\n    /* Default value is 1. */\n}\n#p7 {\n    column-span: 3;\n}\n\nGrid {\n    grid-size: 4 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    height: 100%;\n}\n
    "},{"location":"styles/grid/column_span/#css","title":"CSS","text":"
    column-span: 3;\n
    "},{"location":"styles/grid/column_span/#python","title":"Python","text":"
    widget.styles.column_span = 3\n
    "},{"location":"styles/grid/column_span/#see-also","title":"See also","text":"
    • row-span to specify how many rows a widget spans.
    "},{"location":"styles/grid/grid_columns/","title":"Grid-columns","text":"

    The grid-columns style allows to define the width of the columns of the grid.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_columns/#syntax","title":"Syntax","text":"
    \ngrid-columns: <scalar>+;\n

    The grid-columns style takes one or more <scalar> that specify the length of the columns of the grid.

    If there are more columns in the grid than scalars specified in grid-columns, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

    "},{"location":"styles/grid/grid_columns/#example","title":"Example","text":"

    The example below shows a grid with 10 labels laid out in a grid with 2 rows and 5 columns.

    We set grid-columns: 1fr 16 2fr. Because there are more rows than scalars in the style definition, the scalars will be reused:

    • columns 1 and 4 have width 1fr;
    • columns 2 and 5 have width 16; and
    • column 3 has width 2fr.
    Outputgrid_columns.pygrid_columns.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_columns.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"2fr\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"2fr\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 5 2;\n    grid-columns: 1fr 16 2fr;\n}\n\nLabel {\n    border: round white;\n    content-align-horizontal: center;\n    width: 100%;\n    height: 100%;\n}\n
    "},{"location":"styles/grid/grid_columns/#css","title":"CSS","text":"
    /* Set all columns to have 50% width */\ngrid-columns: 50%;\n\n/* Every other column is twice as wide as the first one */\ngrid-columns: 1fr 2fr;\n
    "},{"location":"styles/grid/grid_columns/#python","title":"Python","text":"
    grid.styles.grid_columns = \"50%\"\ngrid.styles.grid_columns = \"1fr 2fr\"\n
    "},{"location":"styles/grid/grid_columns/#see-also","title":"See also","text":"
    • grid-rows to specify the height of the grid rows.
    "},{"location":"styles/grid/grid_gutter/","title":"Grid-gutter","text":"

    The grid-gutter style sets the size of the gutter in the grid layout. That is, it sets the space between adjacent cells in the grid.

    Gutter is only applied between the edges of cells. No spacing is added between the edges of the cells and the edges of the container.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_gutter/#syntax","title":"Syntax","text":"
    \ngrid-gutter: <integer> [<integer>];\n

    The grid-gutter style takes one or two <integer> that set the length of the gutter along the vertical and horizontal axes. If only one <integer> is supplied, it sets the vertical and horizontal gutters. If two are supplied, they set the vertical and horizontal gutters, respectively.

    "},{"location":"styles/grid/grid_gutter/#example","title":"Example","text":"

    The example below employs a common trick to apply visually consistent spacing around all grid cells.

    Outputgrid_gutter.pygrid_gutter.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25025\u2502\u25026\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25027\u2502\u25028\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_gutter.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n            Label(\"6\"),\n            Label(\"7\"),\n            Label(\"8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 4;\n    grid-gutter: 1 2;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. We set the horizontal gutter to be double the vertical gutter because terminal cells are typically two times taller than they are wide. Thus, the result shows visually consistent spacing around grid cells.
    "},{"location":"styles/grid/grid_gutter/#css","title":"CSS","text":"
    /* Set vertical and horizontal gutters to be the same */\ngrid-gutter: 5;\n\n/* Set vertical and horizontal gutters separately */\ngrid-gutter: 1 2;\n
    "},{"location":"styles/grid/grid_gutter/#python","title":"Python","text":"

    Vertical and horizontal gutters correspond to different Python properties, so they must be set separately:

    widget.styles.grid_gutter_vertical = \"1\"\nwidget.styles.grid_gutter_horizontal = \"2\"\n
    "},{"location":"styles/grid/grid_rows/","title":"Grid-rows","text":"

    The grid-rows style allows to define the height of the rows of the grid.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_rows/#syntax","title":"Syntax","text":"
    \ngrid-rows: <scalar>+;\n

    The grid-rows style takes one or more <scalar> that specify the length of the rows of the grid.

    If there are more rows in the grid than scalars specified in grid-rows, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

    "},{"location":"styles/grid/grid_rows/#example","title":"Example","text":"

    The example below shows a grid with 10 labels laid out in a grid with 5 rows and 2 columns.

    We set grid-rows: 1fr 6 25%. Because there are more rows than scalars in the style definition, the scalars will be reused:

    • rows 1 and 4 have height 1fr;
    • rows 2 and 5 have height 6; and
    • row 3 has height 25%.
    Outputgrid_rows.pygrid_rows.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u250225%\u2502\u250225%\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_rows.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1fr\"),\n            Label(\"1fr\"),\n            Label(\"height = 6\"),\n            Label(\"height = 6\"),\n            Label(\"25%\"),\n            Label(\"25%\"),\n            Label(\"1fr\"),\n            Label(\"1fr\"),\n            Label(\"height = 6\"),\n            Label(\"height = 6\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 5;\n    grid-rows: 1fr 6 25%;\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    "},{"location":"styles/grid/grid_rows/#css","title":"CSS","text":"
    /* Set all rows to have 50% height */\ngrid-rows: 50%;\n\n/* Every other row is twice as tall as the first one */\ngrid-rows: 1fr 2fr;\n
    "},{"location":"styles/grid/grid_rows/#python","title":"Python","text":"
    grid.styles.grid_rows = \"50%\"\ngrid.styles.grid_rows = \"1fr 2fr\"\n
    "},{"location":"styles/grid/grid_rows/#see-also","title":"See also","text":"
    • grid-columns to specify the width of the grid columns.
    "},{"location":"styles/grid/grid_size/","title":"Grid-size","text":"

    The grid-size style sets the number of columns and rows in a grid layout.

    The number of rows can be left unspecified and it will be computed automatically.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_size/#syntax","title":"Syntax","text":"
    \ngrid-size: <integer> [<integer>];\n

    The grid-size style takes one or two non-negative <integer>. The first defines how many columns there are in the grid. If present, the second one sets the number of rows \u2013 regardless of the number of children of the grid \u2013, otherwise the number of rows is computed automatically.

    "},{"location":"styles/grid/grid_size/#examples","title":"Examples","text":""},{"location":"styles/grid/grid_size/#columns-and-rows","title":"Columns and rows","text":"

    In the first example, we create a grid with 2 columns and 5 rows, although we do not have enough labels to fill in the whole grid:

    Outputgrid_size_both.pygrid_size_both.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_size_both.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 4;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. Create a grid with 2 columns and 4 rows.
    "},{"location":"styles/grid/grid_size/#columns-only","title":"Columns only","text":"

    In the second example, we create a grid with 2 columns and however many rows are needed to display all of the grid children:

    Outputgrid_size_columns.pygrid_size_columns.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_size_columns.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. Create a grid with 2 columns and however many rows.
    "},{"location":"styles/grid/grid_size/#css","title":"CSS","text":"
    /* Grid with 3 rows and 5 columns */\ngrid-size: 3 5;\n\n/* Grid with 4 columns and as many rows as needed */\ngrid-size: 4;\n
    "},{"location":"styles/grid/grid_size/#python","title":"Python","text":"

    To programmatically change the grid size, the number of rows and columns must be specified separately:

    widget.styles.grid_size_rows = 3\nwidget.styles.grid_size_columns = 6\n
    "},{"location":"styles/grid/row_span/","title":"Row-span","text":"

    The row-span style specifies how many rows a widget will span in a grid layout.

    Note

    This style only affects widgets that are direct children of a widget with layout: grid.

    "},{"location":"styles/grid/row_span/#syntax","title":"Syntax","text":"
    \nrow-span: <integer>;\n

    The row-span style accepts a single non-negative <integer> that quantifies how many rows the given widget spans.

    "},{"location":"styles/grid/row_span/#example","title":"Example","text":"

    The example below shows a 4 by 4 grid where many placeholders span over several rows.

    Notice that grid cells are filled from left to right, top to bottom. After placing the placeholders #p1, #p2, #p3, and #p4, the next available cell is in the second row, fourth column, which is where the top of #p5 is.

    Outputrow_span.pyrow_span.tcss

    MyApp #p4 #p3 #p2 #p1 #p5 #p6 #p7

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass MyApp(App):\n    CSS_PATH = \"row_span.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(id=\"p1\"),\n            Placeholder(id=\"p2\"),\n            Placeholder(id=\"p3\"),\n            Placeholder(id=\"p4\"),\n            Placeholder(id=\"p5\"),\n            Placeholder(id=\"p6\"),\n            Placeholder(id=\"p7\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    #p1 {\n    row-span: 4;\n}\n#p2 {\n    row-span: 3;\n}\n#p3 {\n    row-span: 2;\n}\n#p4 {\n    row-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p5 {\n    row-span: 3;\n}\n#p6 {\n    row-span: 2;\n}\n#p7 {\n    /* Default value is 1. */\n}\n\nGrid {\n    grid-size: 4 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    height: 100%;\n}\n
    "},{"location":"styles/grid/row_span/#css","title":"CSS","text":"
    row-span: 3\n
    "},{"location":"styles/grid/row_span/#python","title":"Python","text":"
    widget.styles.row_span = 3\n
    "},{"location":"styles/grid/row_span/#see-also","title":"See also","text":"
    • column-span to specify how many columns a widget spans.
    "},{"location":"styles/links/","title":"Links","text":"

    Textual supports the concept of inline \"links\" embedded in text which trigger an action when pressed. There are a number of styles which influence the appearance of these links within a widget.

    Note

    These CSS rules only target Textual action links. Internet hyperlinks are not affected by these styles.

    Property Description link-background The background color of the link text. link-background-hover The background color of the link text when the cursor is over it. link-color The color of the link text. link-color-hover The color of the link text when the cursor is over it. link-style The style of the link text (e.g. underline). link-style-hover The style of the link text when the cursor is over it."},{"location":"styles/links/#syntax","title":"Syntax","text":"
    \nlink-background: <color> [<percentage>];\n\nlink-color: <color> [<percentage>];\n\nlink-style: <text-style>;\n\nlink-background-hover: <color> [<percentage>];\n\nlink-color-hover: <color> [<percentage>];\n\nlink-style-hover: <text-style>;\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/links/#example","title":"Example","text":"

    In the example below, the first label illustrates default link styling. The second label uses CSS to customize the link color, background, and style.

    Outputlinks.pylinks.tcss

    LinksApp Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click! Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nHere is a [@click='app.bell']link[/] which you can click!\n\"\"\"\n\n\nclass LinksApp(App):\n    CSS_PATH = \"links.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n        yield Static(TEXT, id=\"custom\")\n\n\nif __name__ == \"__main__\":\n    app = LinksApp()\n    app.run()\n
    #custom {\n    link-color: black 90%;\n    link-background: dodgerblue;\n    link-style: bold italic underline;\n}\n
    "},{"location":"styles/links/#additional-notes","title":"Additional Notes","text":"
    • Inline links are not widgets, and thus cannot be focused.
    "},{"location":"styles/links/#see-also","title":"See Also","text":"
    • An introduction to links in the Actions guide.
    "},{"location":"styles/links/link_background/","title":"Link-background","text":"

    The link-background style sets the background color of the link.

    Note

    link-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_background/#syntax","title":"Syntax","text":"
    \nlink-background: <color> [<percentage>];\n

    link-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links.

    "},{"location":"styles/links/link_background/#example","title":"Example","text":"

    The example below shows some links with their background color changed. It also shows that link-background does not affect hyperlinks.

    Outputlink_background.pylink_background.tcss

    LinkBackgroundApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkBackgroundApp(App):\n    CSS_PATH = \"link_background.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkBackgroundApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-background rule.
    2. This label has an \"action link\" that can be styled with link-background.
    3. This label has an \"action link\" that can be styled with link-background.
    4. This label has an \"action link\" that can be styled with link-background.
    #lbl1, #lbl2 {\n    link-background: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-background: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-background: $accent;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_background/#css","title":"CSS","text":"
    link-background: red 70%;\nlink-background: $accent;\n
    "},{"location":"styles/links/link_background/#python","title":"Python","text":"
    widget.styles.link_background = \"red 70%\"\nwidget.styles.link_background = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_background = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_background/#see-also","title":"See also","text":"
    • link-color to set the color of link text.
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_background_hover/","title":"Link-background-hover","text":"

    The link-background-hover style sets the background color of the link when the mouse cursor is over the link.

    Note

    link-background-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_background_hover/#syntax","title":"Syntax","text":"
    \nlink-background-hover: <color> [<percentage>];\n

    link-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.

    "},{"location":"styles/links/link_background_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-background-hover set to $accent.

    "},{"location":"styles/links/link_background_hover/#example","title":"Example","text":"

    The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for link-background-hover.

    It also shows that link-background-hover does not affect hyperlinks.

    Outputlink_background_hover.pylink_background_hover.tcss

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_background_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverBackgroundApp(App):\n    CSS_PATH = \"link_background_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverBackgroundApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-background-hover rule.
    2. This label has an \"action link\" that can be styled with link-background-hover.
    3. This label has an \"action link\" that can be styled with link-background-hover.
    4. This label has an \"action link\" that can be styled with link-background-hover.
    #lbl1, #lbl2 {\n    link-background-hover: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-background-hover: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    /* Empty to show the default hover background */ /* (2)! */\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    2. The default behavior for links on hover is to change to a different background color, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
    "},{"location":"styles/links/link_background_hover/#css","title":"CSS","text":"
    link-background-hover: red 70%;\nlink-background-hover: $accent;\n
    "},{"location":"styles/links/link_background_hover/#python","title":"Python","text":"
    widget.styles.link_background_hover = \"red 70%\"\nwidget.styles.link_background_hover = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_background_hover = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_background_hover/#see-also","title":"See also","text":"
    • link-background to set the background color of link text.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_color/","title":"Link-color","text":"

    The link-color style sets the color of the link text.

    Note

    link-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_color/#syntax","title":"Syntax","text":"
    \nlink-color: <color> [<percentage>];\n

    link-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links.

    "},{"location":"styles/links/link_color/#example","title":"Example","text":"

    The example below shows some links with their color changed. It also shows that link-color does not affect hyperlinks.

    Outputlink_color.pylink_color.tcss

    LinkColorApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkColorApp(App):\n    CSS_PATH = \"link_color.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkColorApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-color rule.
    2. This label has an \"action link\" that can be styled with link-color.
    3. This label has an \"action link\" that can be styled with link-color.
    4. This label has an \"action link\" that can be styled with link-color.
    #lbl1, #lbl2 {\n    link-color: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-color: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-color: $accent;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_color/#css","title":"CSS","text":"
    link-color: red 70%;\nlink-color: $accent;\n
    "},{"location":"styles/links/link_color/#python","title":"Python","text":"
    widget.styles.link_color = \"red 70%\"\nwidget.styles.link_color = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_color = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_color/#see-also","title":"See also","text":"
    • link-background to set the background color of link text.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_color_hover/","title":"Link-color-hover","text":"

    The link-color-hover style sets the color of the link text when the mouse cursor is over the link.

    Note

    link-color-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_color_hover/#syntax","title":"Syntax","text":"
    \nlink-color-hover: <color> [<percentage>];\n

    link-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.

    "},{"location":"styles/links/link_color_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-color-hover set to white.

    "},{"location":"styles/links/link_color_hover/#example","title":"Example","text":"

    The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-color-hover does not affect hyperlinks.

    Outputlink_color_hover.pylink_color_hover.tcss

    Note

    The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-background-hover but we haven't done so in this example.

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_color_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverColorApp(App):\n    CSS_PATH = \"link_color_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverColorApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-color-hover rule.
    2. This label has an \"action link\" that can be styled with link-color-hover.
    3. This label has an \"action link\" that can be styled with link-color-hover.
    4. This label has an \"action link\" that can be styled with link-color-hover.
    #lbl1, #lbl2 {\n    link-color-hover: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-color-hover: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-color-hover: black;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_color_hover/#css","title":"CSS","text":"
    link-color-hover: red 70%;\nlink-color-hover: black;\n
    "},{"location":"styles/links/link_color_hover/#python","title":"Python","text":"
    widget.styles.link_color_hover = \"red 70%\"\nwidget.styles.link_color_hover = \"black\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_color_hover = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_color_hover/#see-also","title":"See also","text":"
    • link-color to set the color of link text.
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_style/","title":"Link-style","text":"

    The link-style style sets the text style for the link text.

    Note

    link-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_style/#syntax","title":"Syntax","text":"
    \nlink-style: <text-style>;\n

    link-style will take all the values specified and will apply that styling to text that is enclosed by a Textual action link.

    "},{"location":"styles/links/link_style/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-style set to underline.

    "},{"location":"styles/links/link_style/#example","title":"Example","text":"

    The example below shows some links with different styles applied to their text. It also shows that link-style does not affect hyperlinks.

    Outputlink_style.pylink_style.tcss

    LinkStyleApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkStyleApp(App):\n    CSS_PATH = \"link_style.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkStyleApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-style rule.
    2. This label has an \"action link\" that can be styled with link-style.
    3. This label has an \"action link\" that can be styled with link-style.
    4. This label has an \"action link\" that can be styled with link-style.
    #lbl1, #lbl2 {\n    link-style: bold italic;  /* (1)! */\n}\n\n#lbl3 {\n    link-style: reverse strike;\n}\n\n#lbl4 {\n    link-style: bold;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_style/#css","title":"CSS","text":"
    link-style: bold;\nlink-style: bold italic reverse;\n
    "},{"location":"styles/links/link_style/#python","title":"Python","text":"
    widget.styles.link_style = \"bold\"\nwidget.styles.link_style = \"bold italic reverse\"\n
    "},{"location":"styles/links/link_style/#see-also","title":"See also","text":"
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    • text-style to set the style of text in a widget.
    "},{"location":"styles/links/link_style_hover/","title":"Link-style-hover","text":"

    The link-style-hover style sets the text style for the link text when the mouse cursor is over the link.

    Note

    link-style-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_style_hover/#syntax","title":"Syntax","text":"
    \nlink-style-hover: <text-style>;\n

    link-style-hover applies its <text-style> to the text of Textual action links when the mouse pointer is over them.

    "},{"location":"styles/links/link_style_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-style-hover set to bold.

    "},{"location":"styles/links/link_style_hover/#example","title":"Example","text":"

    The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-style-hover does not affect hyperlinks.

    Outputlink_style_hover.pylink_style_hover.tcss

    Note

    The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-background-hover but we haven't done so in this example.

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_style_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverStyleApp(App):\n    CSS_PATH = \"link_style_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverStyleApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-style-hover rule.
    2. This label has an \"action link\" that can be styled with link-style-hover.
    3. This label has an \"action link\" that can be styled with link-style-hover.
    4. This label has an \"action link\" that can be styled with link-style-hover.
    #lbl1, #lbl2 {\n    link-style-hover: bold italic;  /* (1)! */\n}\n\n#lbl3 {\n    link-style-hover: reverse strike;\n}\n\n#lbl4 {\n    link-style-hover: bold;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    2. The default behavior for links on hover is to change to a different text style, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
    "},{"location":"styles/links/link_style_hover/#css","title":"CSS","text":"
    link-style-hover: bold;\nlink-style-hover: bold italic reverse;\n
    "},{"location":"styles/links/link_style_hover/#python","title":"Python","text":"
    widget.styles.link_style_hover = \"bold\"\nwidget.styles.link_style_hover = \"bold italic reverse\"\n
    "},{"location":"styles/links/link_style_hover/#see-also","title":"See also","text":"
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    • link-style to set the style of link text.
    • text-style to set the style of text in a widget.
    "},{"location":"styles/scrollbar_colors/","title":"Scrollbar colors","text":"

    There are a number of styles to set the colors used in Textual scrollbars. You won't typically need to do this, as the default themes have carefully chosen colors, but you can if you want to.

    Style Applies to scrollbar-background Scrollbar background. scrollbar-background-active Scrollbar background when the thumb is being dragged. scrollbar-background-hover Scrollbar background when the mouse is hovering over it. scrollbar-color Scrollbar \"thumb\" (movable part). scrollbar-color-active Scrollbar thumb when it is active (being dragged). scrollbar-color-hover Scrollbar thumb when the mouse is hovering over it. scrollbar-corner-color The gap between the horizontal and vertical scrollbars."},{"location":"styles/scrollbar_colors/#syntax","title":"Syntax","text":"
    \nscrollbar-background: <color> [<percentage>];\n\nscrollbar-background-active: <color> [<percentage>];\n\nscrollbar-background-hover: <color> [<percentage>];\n\nscrollbar-color: <color> [<percentage>];\n\nscrollbar-color-active: <color> [<percentage>];\n\nscrollbar-color-hover: <color> [<percentage>];\n\nscrollbar-corner-color: <color> [<percentage>];\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/scrollbar_colors/#example","title":"Example","text":"

    This example shows two panels that contain oversized text. The right panel sets scrollbar-background, scrollbar-color, and scrollbar-corner-color, and the left panel shows the default colors for comparison.

    Outputscrollbars.pyscrollbars.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.\u2583\u2583see\u00a0its\u00a0path.\u2583\u2583 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t \u258d\u258d

    from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbars.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            ScrollableContainer(Label(TEXT * 10)),\n            ScrollableContainer(Label(TEXT * 10), classes=\"right\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    Label {\n    width: 150%;\n    height: 150%;\n}\n\n.right {\n    scrollbar-background: red;\n    scrollbar-color: green;\n    scrollbar-corner-color: blue;\n}\n\nHorizontal > ScrollableContainer {\n    width: 50%;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/","title":"Scrollbar-background","text":"

    The scrollbar-background style sets the background color of the scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_background/#syntax","title":"Syntax","text":"
    \nscrollbar-background: <color> [<percentage>];\n

    scrollbar-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_background/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#css","title":"CSS","text":"
    scrollbar-backround: blue;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#python","title":"Python","text":"
    widget.styles.scrollbar_background = \"blue\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#see-also","title":"See also","text":"
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/","title":"Scrollbar-background-active","text":"

    The scrollbar-background-active style sets the background color of the scrollbar when the thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#syntax","title":"Syntax","text":"
    \nscrollbar-background-active: <color> [<percentage>];\n

    scrollbar-background-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when its thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#css","title":"CSS","text":"
    scrollbar-backround-active: red;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#python","title":"Python","text":"
    widget.styles.scrollbar_background_active = \"red\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/","title":"Scrollbar-background-hover","text":"

    The scrollbar-background-hover style sets the background color of the scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#syntax","title":"Syntax","text":"
    \nscrollbar-background-hover: <color> [<percentage>];\n

    scrollbar-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#css","title":"CSS","text":"
    scrollbar-background-hover: purple;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#python","title":"Python","text":"
    widget.styles.scrollbar_background_hover = \"purple\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also","title":"See also","text":""},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also_1","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    "},{"location":"styles/scrollbar_colors/scrollbar_color/","title":"Scrollbar-color","text":"

    The scrollbar-color style sets the color of the scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_color/#syntax","title":"Syntax","text":"
    \nscrollbar-color: <color> [<percentage>];\n

    scrollbar-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_color/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#css","title":"CSS","text":"
    scrollbar-color: cyan;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#python","title":"Python","text":"
    widget.styles.scrollbar_color = \"cyan\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/","title":"Scrollbar-color-active","text":"

    The scrollbar-color-active style sets the color of the scrollbar when the thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#syntax","title":"Syntax","text":"
    \nscrollbar-color-active: <color> [<percentage>];\n

    scrollbar-color-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when its thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#css","title":"CSS","text":"
    scrollbar-color-active: yellow;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#python","title":"Python","text":"
    widget.styles.scrollbar_color_active = \"yellow\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#see-also","title":"See also","text":"
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/","title":"Scrollbar-color-hover","text":"

    The scrollbar-color-hover style sets the color of the scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#syntax","title":"Syntax","text":"
    \nscrollbar-color-hover: <color> [<percentage>];\n

    scrollbar-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#css","title":"CSS","text":"
    scrollbar-color-hover: pink;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#python","title":"Python","text":"
    widget.styles.scrollbar_color_hover = \"pink\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#see-also","title":"See also","text":"
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/","title":"Scrollbar-corner-color","text":"

    The scrollbar-corner-color style sets the color of the gap between the horizontal and vertical scrollbars.

    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#syntax","title":"Syntax","text":"
    \nscrollbar-corner-color: <color> [<percentage>];\n

    scrollbar-corner-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of the gap between the horizontal and vertical scrollbars of a widget.

    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#example","title":"Example","text":"

    The example below sets the scrollbar corner (bottom-right corner of the screen) to white.

    Outputscrollbar_corner_color.pyscrollbar_corner_color.tcss

    ScrollbarCornerColorApp I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarCornerColorApp(App):\n    CSS_PATH = \"scrollbar_corner_color.tcss\"\n\n    def compose(self):\n        yield Label(TEXT.replace(\"\\n\", \" \") + \"\\n\" + TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarCornerColorApp()\n    app.run()\n
    Screen {\n    overflow: auto auto;\n    scrollbar-corner-color: white;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#css","title":"CSS","text":"
    scrollbar-corner-color: white;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#python","title":"Python","text":"
    widget.styles.scrollbar_corner_color = \"white\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-color to set the color of scrollbars.
    "},{"location":"widgets/","title":"Widgets","text":"

    A reference to the builtin widgets.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"widgets/button/","title":"Button","text":"

    A simple button widget which can be pressed using a mouse click or by pressing Enter when it has focus.

    • Focusable
    • Container
    "},{"location":"widgets/button/#example","title":"Example","text":"

    The example below shows each button variant, and its disabled equivalent. Clicking any of the non-disabled buttons in the example app below will result in the app exiting and the details of the selected button being printed to the console.

    Outputbutton.pybutton.tcss

    ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DefaultDefault \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Primary!Primary! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Success!Success! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Warning!Warning! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Error!Error! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, Static\n\n\nclass ButtonsApp(App[str]):\n    CSS_PATH = \"button.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            VerticalScroll(\n                Static(\"Standard Buttons\", classes=\"header\"),\n                Button(\"Default\"),\n                Button(\"Primary!\", variant=\"primary\"),\n                Button.success(\"Success!\"),\n                Button.warning(\"Warning!\"),\n                Button.error(\"Error!\"),\n            ),\n            VerticalScroll(\n                Static(\"Disabled Buttons\", classes=\"header\"),\n                Button(\"Default\", disabled=True),\n                Button(\"Primary!\", variant=\"primary\", disabled=True),\n                Button.success(\"Success!\", disabled=True),\n                Button.warning(\"Warning!\", disabled=True),\n                Button.error(\"Error!\", disabled=True),\n            ),\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(str(event.button))\n\n\nif __name__ == \"__main__\":\n    app = ButtonsApp()\n    print(app.run())\n
    Button {\n    margin: 1 2;\n}\n\nHorizontal > VerticalScroll {\n    width: 24;\n}\n\n.header {\n    margin: 1 0 0 2;\n    text-style: bold;\n}\n
    "},{"location":"widgets/button/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description label str \"\" The text that appears inside the button. variant ButtonVariant \"default\" Semantic styling variant. One of default, primary, success, warning, error. disabled bool False Whether the button is disabled or not. Disabled buttons cannot be focused or clicked, and are styled in a way that suggests this."},{"location":"widgets/button/#messages","title":"Messages","text":"
    • Button.Pressed
    "},{"location":"widgets/button/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/button/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/button/#additional-notes","title":"Additional Notes","text":"
    • The spacing between the text and the edges of a button are not due to padding. The default styling for a Button has the height set to 3 lines and a min-width of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with border: none;.
    "},{"location":"widgets/button/#textual.widgets.Button","title":"textual.widgets.Button class","text":"
    def __init__(\n    self,\n    label=None,\n    variant=\"default\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A simple clickable button.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    variant ButtonVariant 'default'

    The variant of the button.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    "},{"location":"widgets/button/#textual.widgets._button.Button.active_effect_duration","title":"active_effect_duration instance-attribute","text":"
    active_effect_duration = 0.3\n

    Amount of time in seconds the button 'press' animation lasts.

    "},{"location":"widgets/button/#textual.widgets._button.Button.label","title":"label instance-attribute class-attribute","text":"
    label: reactive[TextType] = label\n

    The text label that appears within the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.variant","title":"variant instance-attribute class-attribute","text":"
    variant = variant\n

    The variant name for the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed","title":"Pressed class","text":"
    def __init__(self, button):\n

    Bases: Message

    Event sent when a Button is pressed.

    Can be handled using on_button_pressed in a subclass of Button or in a parent widget in the DOM.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.button","title":"button instance-attribute","text":"
    button: Button = button\n

    The button that was pressed.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.control","title":"control property","text":"
    control: Button\n

    An alias for Pressed.button.

    This will be the same value as Pressed.button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.action_press","title":"action_press method","text":"
    def action_press(self):\n

    Activate a press of the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.error","title":"error classmethod","text":"
    def error(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating an error Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'error' variant.

    "},{"location":"widgets/button/#textual.widgets._button.Button.press","title":"press method","text":"
    def press(self):\n

    Respond to a button press.

    Returns Type Description Self

    The button instance.

    "},{"location":"widgets/button/#textual.widgets._button.Button.success","title":"success classmethod","text":"
    def success(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating a success Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'success' variant.

    "},{"location":"widgets/button/#textual.widgets._button.Button.validate_label","title":"validate_label method","text":"
    def validate_label(self, label):\n

    Parse markup for self.label

    "},{"location":"widgets/button/#textual.widgets._button.Button.warning","title":"warning classmethod","text":"
    def warning(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating a warning Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'warning' variant.

    "},{"location":"widgets/button/#textual.widgets.button","title":"textual.widgets.button","text":""},{"location":"widgets/button/#textual.widgets.button.ButtonVariant","title":"ButtonVariant module-attribute","text":"
    ButtonVariant = Literal[\n    \"default\", \"primary\", \"success\", \"warning\", \"error\"\n]\n

    The names of the valid button variants.

    These are the variants that can be used with a Button.

    "},{"location":"widgets/checkbox/","title":"Checkbox","text":"

    Added in version 0.13.0

    A simple checkbox widget which stores a boolean value.

    • Focusable
    • Container
    "},{"location":"widgets/checkbox/#example","title":"Example","text":"

    The example below shows check boxes in various states.

    Outputcheckbox.pycheckbox.tcss

    CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Checkbox\n\n\nclass CheckboxApp(App[None]):\n    CSS_PATH = \"checkbox.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            yield Checkbox(\"Arrakis :sweat:\")\n            yield Checkbox(\"Caladan\")\n            yield Checkbox(\"Chusuk\")\n            yield Checkbox(\"[b]Giedi Prime[/b]\")\n            yield Checkbox(\"[magenta]Ginaz[/]\")\n            yield Checkbox(\"Grumman\", True)\n            yield Checkbox(\"Kaitain\", id=\"initial_focus\")\n            yield Checkbox(\"Novebruns\", True)\n\n    def on_mount(self):\n        self.query_one(\"#initial_focus\", Checkbox).focus()\n\n\nif __name__ == \"__main__\":\n    CheckboxApp().run()\n
    Screen {\n    align: center middle;\n}\n\nVerticalScroll {\n    width: auto;\n    height: auto;\n    background: $boost;\n    padding: 2;\n}\n
    "},{"location":"widgets/checkbox/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the checkbox."},{"location":"widgets/checkbox/#messages","title":"Messages","text":"
    • Checkbox.Changed
    "},{"location":"widgets/checkbox/#bindings","title":"Bindings","text":"

    The checkbox widget defines the following bindings:

    Key(s) Description enter, space Toggle the value."},{"location":"widgets/checkbox/#component-classes","title":"Component Classes","text":"

    The checkbox widget inherits the following component classes:

    Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/checkbox/#textual.widgets.Checkbox","title":"textual.widgets.Checkbox class","text":"

    Bases: ToggleButton

    A check box widget that represents a boolean value.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed","title":"Changed class","text":"

    Bases: ToggleButton.Changed

    Posted when the value of the checkbox changes.

    This message can be handled using an on_checkbox_changed method.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.checkbox","title":"checkbox property","text":"
    checkbox: Checkbox\n

    The checkbox that was changed.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.control","title":"control property","text":"
    control: Checkbox\n

    An alias for Changed.checkbox.

    "},{"location":"widgets/collapsible/","title":"Collapsible","text":"

    Added in version 0.37

    A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing Enter.

    • Focusable
    • Container
    "},{"location":"widgets/collapsible/#composing","title":"Composing","text":"

    You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (with statement).

    Here is an example of using the constructor to add content:

    def compose(self) -> ComposeResult:\n    yield Collapsible(Label(\"Hello, world.\"))\n

    Here's how the to use it with the context manager:

    def compose(self) -> ComposeResult:\n    with Collapsible():\n        yield Label(\"Hello, world.\")\n

    The second form is generally preferred, but the end result is the same.

    "},{"location":"widgets/collapsible/#title","title":"Title","text":"

    The default title \"Toggle\" can be customized by setting the title parameter of the constructor:

    def compose(self) -> ComposeResult:\n    with Collapsible(title=\"An interesting story.\"):\n        yield Label(\"Interesting but verbose story.\")\n
    "},{"location":"widgets/collapsible/#initial-state","title":"Initial State","text":"

    The initial state of the Collapsible widget can be customized via the collapsed parameter of the constructor:

    def compose(self) -> ComposeResult:\n    with Collapsible(title=\"Contents 1\", collapsed=False):\n        yield Label(\"Hello, world.\")\n\n    with Collapsible(title=\"Contents 2\", collapsed=True):  # Default.\n        yield Label(\"Hello, world.\")\n
    "},{"location":"widgets/collapsible/#collapseexpand-symbols","title":"Collapse/Expand Symbols","text":"

    The symbols used to show the collapsed / expanded state can be customized by setting the parameters collapsed_symbol and expanded_symbol:

    def compose(self) -> ComposeResult:\n    with Collapsible(collapsed_symbol=\">>>\", expanded_symbol=\"v\"):\n        yield Label(\"Hello, world.\")\n
    "},{"location":"widgets/collapsible/#examples","title":"Examples","text":"

    The following example contains three Collapsibles in different states.

    All expandedAll collapsedMixedcollapsible.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Paul \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Leto \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Jessica \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Footer, Label, Markdown\n\nLETO = \"\"\"\\\n# Duke Leto I Atreides\n\nHead of House Atreides.\"\"\"\n\nJESSICA = \"\"\"\n# Lady Jessica\n\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\n\nPAUL = \"\"\"\n# Paul Atreides\n\nSon of Leto and Jessica.\n\"\"\"\n\n\nclass CollapsibleApp(App[None]):\n    \"\"\"An example of collapsible container.\"\"\"\n\n    BINDINGS = [\n        (\"c\", \"collapse_or_expand(True)\", \"Collapse All\"),\n        (\"e\", \"collapse_or_expand(False)\", \"Expand All\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Compose app with collapsible containers.\"\"\"\n        yield Footer()\n        with Collapsible(collapsed=False, title=\"Leto\"):\n            yield Label(LETO)\n        yield Collapsible(Markdown(JESSICA), collapsed=False, title=\"Jessica\")\n        with Collapsible(collapsed=True, title=\"Paul\"):\n            yield Markdown(PAUL)\n\n    def action_collapse_or_expand(self, collapse: bool) -> None:\n        for child in self.walk_children(Collapsible):\n            child.collapsed = collapse\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#setting-initial-state","title":"Setting Initial State","text":"

    The example below shows nested Collapsible widgets and how to set their initial state.

    Outputcollapsible_nested.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Toggle \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Toggle

    from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Label\n\n\nclass CollapsibleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        with Collapsible(collapsed=False):\n            with Collapsible():\n                yield Label(\"Hello, world.\")\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#custom-symbols","title":"Custom Symbols","text":"

    The following example shows Collapsible widgets with custom expand/collapse symbols.

    Outputcollapsible_custom_symbol.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 >>>\u00a0Togglev\u00a0Toggle Hello,\u00a0world.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Collapsible, Label\n\n\nclass CollapsibleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            with Collapsible(\n                collapsed_symbol=\">>>\",\n                expanded_symbol=\"v\",\n            ):\n                yield Label(\"Hello, world.\")\n\n            with Collapsible(\n                collapsed_symbol=\">>>\",\n                expanded_symbol=\"v\",\n                collapsed=False,\n            ):\n                yield Label(\"Hello, world.\")\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description collapsed bool True Controls the collapsed/expanded state of the widget. title str \"Toggle\" Title of the collapsed/expanded contents."},{"location":"widgets/collapsible/#messages","title":"Messages","text":"
    • Collapsible.Toggled
    "},{"location":"widgets/collapsible/#bindings","title":"Bindings","text":"

    The collapsible widget defines the following binding on its title:

    Key(s) Description enter Toggle the collapsible."},{"location":"widgets/collapsible/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/collapsible/#textual.widgets.Collapsible","title":"textual.widgets.Collapsible class","text":"
    def __init__(\n    self,\n    *children,\n    title=\"Toggle\",\n    collapsed=True,\n    collapsed_symbol=\"\u25b6\",\n    expanded_symbol=\"\u25bc\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A collapsible container.

    Parameters Parameter Default Description *children Widget ()

    Contents that will be collapsed/expanded.

    title str 'Toggle'

    Title of the collapsed/expanded contents.

    collapsed bool True

    Default status of the contents.

    collapsed_symbol str '\u25b6'

    Collapsed symbol before the title.

    expanded_symbol str '\u25bc'

    Expanded symbol before the title.

    name str | None None

    The name of the collapsible.

    id str | None None

    The ID of the collapsible in the DOM.

    classes str | None None

    The CSS classes of the collapsible.

    disabled bool False

    Whether the collapsible is disabled or not.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Collapsed","title":"Collapsed class","text":"

    Bases: Toggled

    Event sent when the Collapsible widget is collapsed.

    Can be handled using on_collapsible_collapsed in a subclass of Collapsible or in a parent widget in the DOM.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Expanded","title":"Expanded class","text":"

    Bases: Toggled

    Event sent when the Collapsible widget is expanded.

    Can be handled using on_collapsible_expanded in a subclass of Collapsible or in a parent widget in the DOM.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled","title":"Toggled class","text":"
    def __init__(self, collapsible):\n

    Bases: Message

    Parent class subclassed by Collapsible messages.

    Can be handled with on(Collapsible.Toggled) if you want to handle expansions and collapsed in the same way, or you can handle the specific events individually.

    Parameters Parameter Default Description collapsible Collapsible required

    The Collapsible widget that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled.collapsible","title":"collapsible instance-attribute","text":"
    collapsible: Collapsible = collapsible\n

    The collapsible that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled.control","title":"control property","text":"
    control: Collapsible\n

    An alias for Toggled.collapsible.

    "},{"location":"widgets/content_switcher/","title":"ContentSwitcher","text":"

    Added in version 0.14.0

    A widget for containing and switching display between multiple child widgets.

    • Focusable
    • Container
    "},{"location":"widgets/content_switcher/#example","title":"Example","text":"

    The example below uses a ContentSwitcher in combination with two Buttons to create a simple tabbed view. Note how each Button has an ID set, and how each child of the ContentSwitcher has a corresponding ID; then a Button.Clicked handler is used to set ContentSwitcher.current to switch between the different views.

    Outputcontent_switcher.pycontent_switcher.tcss

    ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DataTableMarkdown \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u00a0Book\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Year\u00a0\u2502 \u2502\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01965\u00a0\u2502 \u2502\u00a0Dune\u00a0Messiah\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01969\u00a0\u2502 \u2502\u00a0Children\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01976\u00a0\u2502 \u2502\u00a0God\u00a0Emperor\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01981\u00a0\u2502 \u2502\u00a0Heretics\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01984\u00a0\u2502 \u2502\u00a0Chapterhouse:\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01985\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, ContentSwitcher, DataTable, Markdown\n\nMARKDOWN_EXAMPLE = \"\"\"# Three Flavours Cornetto\n\nThe Three Flavours Cornetto trilogy is an anthology series of British\ncomedic genre films directed by Edgar Wright.\n\n## Shaun of the Dead\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Strawberry | 2004-04-09 | Edgar Wright |\n\n## Hot Fuzz\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Classico | 2007-02-17 | Edgar Wright |\n\n## The World's End\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Mint | 2013-07-19 | Edgar Wright |\n\"\"\"\n\n\nclass ContentSwitcherApp(App[None]):\n    CSS_PATH = \"content_switcher.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"buttons\"):  # (1)!\n            yield Button(\"DataTable\", id=\"data-table\")  # (2)!\n            yield Button(\"Markdown\", id=\"markdown\")  # (3)!\n\n        with ContentSwitcher(initial=\"data-table\"):  # (4)!\n            yield DataTable(id=\"data-table\")\n            with VerticalScroll(id=\"markdown\"):\n                yield Markdown(MARKDOWN_EXAMPLE)\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.query_one(ContentSwitcher).current = event.button.id  # (5)!\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(\"Book\", \"Year\")\n        table.add_rows(\n            [\n                (title.ljust(35), year)\n                for title, year in (\n                    (\"Dune\", 1965),\n                    (\"Dune Messiah\", 1969),\n                    (\"Children of Dune\", 1976),\n                    (\"God Emperor of Dune\", 1981),\n                    (\"Heretics of Dune\", 1984),\n                    (\"Chapterhouse: Dune\", 1985),\n                )\n            ]\n        )\n\n\nif __name__ == \"__main__\":\n    ContentSwitcherApp().run()\n
    1. A Horizontal to hold the buttons, each with a unique ID.
    2. This button will select the DataTable in the ContentSwitcher.
    3. This button will select the Markdown in the ContentSwitcher.
    4. Note that the initial visible content is set by its ID, see below.
    5. When a button is pressed, its ID is used to switch to a different widget in the ContentSwitcher. Remember that IDs are unique within parent, so the buttons and the widgets in the ContentSwitcher can share IDs.
    Screen {\n    align: center middle;\n    padding: 1;\n}\n\n#buttons {\n    height: 3;\n    width: auto;\n}\n\nContentSwitcher {\n    background: $panel;\n    border: round $primary;\n    width: 90%;\n    height: 1fr;\n}\n\nDataTable {\n    background: $panel;\n}\n\nMarkdownH2 {\n    background: $primary;\n    color: yellow;\n    border: none;\n    padding: 0;\n}\n

    When the user presses the \"Markdown\" button the view is switched:

    ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DataTableMarkdown \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eThree\u00a0Flavours\u00a0Cornetto\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u2502 \u2502of\u00a0British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Shaun\u00a0of\u00a0the\u00a0Dead\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eStrawberry\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hot\u00a0Fuzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eClassico\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0The\u00a0World's\u00a0End\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    "},{"location":"widgets/content_switcher/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description current str | None None The ID of the currently-visible child. None means nothing is visible."},{"location":"widgets/content_switcher/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/content_switcher/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/content_switcher/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current instance-attribute class-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"widgets/data_table/","title":"DataTable","text":"

    A table widget optimized for displaying a lot of data.

    • Focusable
    • Container
    "},{"location":"widgets/data_table/#guide","title":"Guide","text":""},{"location":"widgets/data_table/#adding-data","title":"Adding data","text":"

    The following example shows how to fill a table with data. First, we use add_columns to include the lane, swimmer, country, and time columns in the table. After that, we use the add_rows method to insert the rows into the table.

    Outputdata_table.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        table.add_rows(ROWS[1:])\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    To add a single row or column use add_row and add_column, respectively.

    "},{"location":"widgets/data_table/#styling-and-justifying-cells","title":"Styling and justifying cells","text":"

    Cells can contain more than just plain strings - Rich renderables such as Text are also supported. Text objects provide an easy way to style and justify cell content:

    Outputdata_table_renderables.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0Singapore50.39 \u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0PhelpsUnited\u00a0States51.14 \u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0South\u00a0Africa51.14 \u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary51.14 \u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China51.26 \u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France51.58 \u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0ShieldsUnited\u00a0States51.73 \u00a0\u00a0\u00a01Aleksandr\u00a0Sadovnikov\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Russia51.84 \u00a0\u00a010\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0Scotland51.84

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        for row in ROWS[1:]:\n            # Adding styled and justified `Text` objects instead of plain strings.\n            styled_row = [\n                Text(str(cell), style=\"italic #03AC13\", justify=\"right\") for cell in row\n            ]\n            table.add_row(*styled_row)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#keys","title":"Keys","text":"

    When adding a row to the table, you can supply a key to add_row. A key is a unique identifier for that row. If you don't supply a key, Textual will generate one for you and return it from add_row. This key can later be used to reference the row, regardless of its current position in the table.

    When working with data from a database, for example, you may wish to set the row key to the primary key of the data to ensure uniqueness. The method add_column also accepts a key argument and works similarly.

    Keys are important because cells in a data table can change location due to factors like row deletion and sorting. Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table.

    If you want to change the table based solely on coordinates, you can use the coordinate_to_cell_key method to convert a coordinate to a cell key, which is a (row_key, column_key) pair.

    "},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

    The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute. Three types of cursors are supported: cell, row, and column. Change the cursor type by assigning to the cursor_type reactive attribute.

    Column CursorRow CursorCell Cursordata_table_cursors.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\ncursors = cycle([\"column\", \"row\", \"cell\"])\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.cursor_type = next(cursors)\n        table.zebra_stripes = True\n        table.add_columns(*ROWS[0])\n        table.add_rows(ROWS[1:])\n\n    def key_c(self):\n        table = self.query_one(DataTable)\n        table.cursor_type = next(cursors)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    You can change the position of the cursor using the arrow keys, Page Up, Page Down, Home and End, or by assigning to the cursor_coordinate reactive attribute.

    "},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

    Cells can be updated in the DataTable by using the update_cell and update_cell_at methods.

    "},{"location":"widgets/data_table/#removing-data","title":"Removing data","text":"

    To remove all data in the table, use the clear method. To remove individual rows, use remove_row. The remove_row method accepts a key argument, which identifies the row to be removed.

    If you wish to remove the row below the cursor in the DataTable, use coordinate_to_cell_key to get the row key of the row under the current cursor_coordinate, then supply this key to remove_row:

    # Get the keys for the row and column under the cursor.\nrow_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the row key to `remove_row` to delete the row.\ntable.remove_row(row_key)\n
    "},{"location":"widgets/data_table/#removing-columns","title":"Removing columns","text":"

    To remove individual columns, use remove_column. The remove_column method accepts a key argument, which identifies the column to be removed.

    You can remove the column below the cursor using the same coordinate_to_cell_key method described above:

    # Get the keys for the row and column under the cursor.\n_, column_key = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the column key to `column_row` to delete the column.\ntable.remove_column(column_key)\n
    "},{"location":"widgets/data_table/#fixed-data","title":"Fixed data","text":"

    You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. To do this, assign an integer to the fixed_rows or fixed_columns reactive attributes of the DataTable.

    Fixed datadata_table_fixed.py

    TableApp \u00a0A\u00a0\u00a0\u00a0B\u00a0\u00a0\u00a0\u00a0C\u00a0\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0 \u00a079\u00a0\u00a0158\u00a0\u00a0237\u00a0 \u00a080\u00a0\u00a0160\u00a0\u00a0240\u00a0 \u00a081\u00a0\u00a0162\u00a0\u00a0243\u00a0 \u00a082\u00a0\u00a0164\u00a0\u00a0246\u00a0 \u00a083\u00a0\u00a0166\u00a0\u00a0249\u00a0 \u00a084\u00a0\u00a0168\u00a0\u00a0252\u00a0 \u00a085\u00a0\u00a0170\u00a0\u00a0255\u00a0 \u00a086\u00a0\u00a0172\u00a0\u00a0258\u00a0 \u00a087\u00a0\u00a0174\u00a0\u00a0261\u00a0 \u00a088\u00a0\u00a0176\u00a0\u00a0264\u00a0 \u00a089\u00a0\u00a0178\u00a0\u00a0267\u00a0 \u00a090\u00a0\u00a0180\u00a0\u00a0270\u00a0 \u00a091\u00a0\u00a0182\u00a0\u00a0273\u00a0 \u00a092\u00a0\u00a0184\u00a0\u00a0276\u00a0 \u00a093\u00a0\u00a0186\u00a0\u00a0279\u00a0 \u00a094\u00a0\u00a0188\u00a0\u00a0282\u00a0\u2587\u2587 \u00a095\u00a0\u00a0190\u00a0\u00a0285\u00a0 \u00a096\u00a0\u00a0192\u00a0\u00a0288\u00a0 \u00a097\u00a0\u00a0194\u00a0\u00a0291\u00a0 \u00a098\u00a0\u00a0196\u00a0\u00a0294\u00a0 \u00a099\u00a0\u00a0198\u00a0\u00a0297\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\n\nclass TableApp(App):\n    CSS = \"DataTable {height: 1fr}\"\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.focus()\n        table.add_columns(\"A\", \"B\", \"C\")\n        for number in range(1, 100):\n            table.add_row(str(number), str(number * 2), str(number * 3))\n        table.fixed_rows = 2\n        table.fixed_columns = 1\n        table.cursor_type = \"row\"\n        table.zebra_stripes = True\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    In the example above, we set fixed_rows to 2, and fixed_columns to 1, meaning the first two rows and the leftmost column do not scroll - they always remain visible as you scroll through the data table.

    "},{"location":"widgets/data_table/#sorting","title":"Sorting","text":"

    The DataTable can be sorted using the sort method. In order to sort your data by a column, you can provide the key you supplied to the add_column method or a ColumnKey. You can then pass one more column keys to the sort method to sort by one or more columns.

    Additionally, you can sort your DataTable with a custom function (or other callable) via the key argument. Similar to the key parameter of the built-in sorted() function, your function (or other callable) should take a single argument (row) and return a key to use for sorting purposes.

    Providing both columns and key will limit the row information sent to your key function (or other callable) to only the columns specified.

    Outputdata_table_sort.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a01\u00a0\u00a0time\u00a02\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a050.39\u00a0\u00a0\u00a051.84\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a050.39\u00a0\u00a0\u00a051.84\u00a0\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a051.14\u00a0\u00a0\u00a051.73\u00a0\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a051.14\u00a0\u00a0\u00a051.58\u00a0\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a051.26\u00a0\u00a0\u00a051.26\u00a0\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a051.58\u00a0\u00a0\u00a052.15\u00a0\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a051.73\u00a0\u00a0\u00a051.12\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0Russia\u00a051.84\u00a0\u00a0\u00a050.85\u00a0\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a051.84\u00a0\u00a0\u00a051.55\u00a0\u00a0 \u00a0A\u00a0\u00a0Sort\u00a0By\u00a0Average\u00a0Time\u00a0\u00a0N\u00a0\u00a0Sort\u00a0By\u00a0Last\u00a0Name\u00a0\u00a0C\u00a0\u00a0Sort\u00a0By\u00a0Country\u00a0\u00a0D\u00a0\u00a0Sort\u00a0By\u00a0\u2026

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable, Footer\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time 1\", \"time 2\"),\n    (4, \"Joseph Schooling\", Text(\"Singapore\", style=\"italic\"), 50.39, 51.84),\n    (2, \"Michael Phelps\", Text(\"United States\", style=\"italic\"), 50.39, 51.84),\n    (5, \"Chad le Clos\", Text(\"South Africa\", style=\"italic\"), 51.14, 51.73),\n    (6, \"L\u00e1szl\u00f3 Cseh\", Text(\"Hungary\", style=\"italic\"), 51.14, 51.58),\n    (3, \"Li Zhuhao\", Text(\"China\", style=\"italic\"), 51.26, 51.26),\n    (8, \"Mehdy Metella\", Text(\"France\", style=\"italic\"), 51.58, 52.15),\n    (7, \"Tom Shields\", Text(\"United States\", style=\"italic\"), 51.73, 51.12),\n    (1, \"Aleksandr Sadovnikov\", Text(\"Russia\", style=\"italic\"), 51.84, 50.85),\n    (10, \"Darren Burns\", Text(\"Scotland\", style=\"italic\"), 51.84, 51.55),\n]\n\n\nclass TableApp(App):\n    BINDINGS = [\n        (\"a\", \"sort_by_average_time\", \"Sort By Average Time\"),\n        (\"n\", \"sort_by_last_name\", \"Sort By Last Name\"),\n        (\"c\", \"sort_by_country\", \"Sort By Country\"),\n        (\"d\", \"sort_by_columns\", \"Sort By Columns (Only)\"),\n    ]\n\n    current_sorts: set = set()\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        for col in ROWS[0]:\n            table.add_column(col, key=col)\n        table.add_rows(ROWS[1:])\n\n    def sort_reverse(self, sort_type: str):\n        \"\"\"Determine if `sort_type` is ascending or descending.\"\"\"\n        reverse = sort_type in self.current_sorts\n        if reverse:\n            self.current_sorts.remove(sort_type)\n        else:\n            self.current_sorts.add(sort_type)\n        return reverse\n\n    def action_sort_by_average_time(self) -> None:\n        \"\"\"Sort DataTable by average of times (via a function) and\n        passing of column data through positional arguments.\"\"\"\n\n        def sort_by_average_time_then_last_name(row_data):\n            name, *scores = row_data\n            return (sum(scores) / len(scores), name.split()[-1])\n\n        table = self.query_one(DataTable)\n        table.sort(\n            \"swimmer\",\n            \"time 1\",\n            \"time 2\",\n            key=sort_by_average_time_then_last_name,\n            reverse=self.sort_reverse(\"time\"),\n        )\n\n    def action_sort_by_last_name(self) -> None:\n        \"\"\"Sort DataTable by last name of swimmer (via a lambda).\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\n            \"swimmer\",\n            key=lambda swimmer: swimmer.split()[-1],\n            reverse=self.sort_reverse(\"swimmer\"),\n        )\n\n    def action_sort_by_country(self) -> None:\n        \"\"\"Sort DataTable by country which is a `Rich.Text` object.\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\n            \"country\",\n            key=lambda country: country.plain,\n            reverse=self.sort_reverse(\"country\"),\n        )\n\n    def action_sort_by_columns(self) -> None:\n        \"\"\"Sort DataTable without a key.\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\"swimmer\", \"lane\", reverse=self.sort_reverse(\"columns\"))\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#labelled-rows","title":"Labelled rows","text":"

    A \"label\" can be attached to a row using the add_row method. This will add an extra column to the left of the table which the cursor cannot interact with. This column is similar to the leftmost column in a spreadsheet containing the row numbers. The example below shows how to attach simple numbered labels to rows.

    Labelled rowsdata_table_labels.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 1\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 2\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 3\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 4\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 5\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 6\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 7\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 8\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 9\u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        for number, row in enumerate(ROWS[1:], start=1):\n            label = Text(str(number), style=\"#B0FC38 italic\")\n            table.add_row(*row, label=label)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_header bool True Show the table header show_row_labels bool True Show the row labels (if applicable) fixed_rows int 0 Number of fixed rows (rows which do not scroll) fixed_columns int 0 Number of fixed columns (columns which do not scroll) zebra_stripes bool False Display alternating colors on rows header_height int 1 Height of header row show_cursor bool True Show the cursor cursor_type str \"cell\" One of \"cell\", \"row\", \"column\", or \"none\" cursor_coordinate Coordinate Coordinate(0, 0) The current coordinate of the cursor hover_coordinate Coordinate Coordinate(0, 0) The coordinate the mouse cursor is above"},{"location":"widgets/data_table/#messages","title":"Messages","text":"
    • DataTable.CellHighlighted
    • DataTable.CellSelected
    • DataTable.RowHighlighted
    • DataTable.RowSelected
    • DataTable.ColumnHighlighted
    • DataTable.ColumnSelected
    • DataTable.HeaderSelected
    • DataTable.RowLabelSelected
    "},{"location":"widgets/data_table/#bindings","title":"Bindings","text":"

    The data table widget defines the following bindings:

    Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#component-classes","title":"Component Classes","text":"

    The data table widget provides the following component classes:

    Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets.DataTable","title":"textual.widgets.DataTable class","text":"
    def __init__(\n    self,\n    *,\n    show_header=True,\n    show_row_labels=True,\n    fixed_rows=0,\n    fixed_columns=0,\n    zebra_stripes=False,\n    header_height=1,\n    show_cursor=True,\n    cursor_foreground_priority=\"css\",\n    cursor_background_priority=\"renderable\",\n    cursor_type=\"cell\",\n    cell_padding=_DEFAULT_CELL_X_PADDING,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView, Generic[CellType]

    A tabular widget that contains data.

    Parameters Parameter Default Description show_header bool True

    Whether the table header should be visible or not.

    show_row_labels bool True

    Whether the row labels should be shown or not.

    fixed_rows int 0

    The number of rows, counting from the top, that should be fixed and still visible when the user scrolls down.

    fixed_columns int 0

    The number of columns, counting from the left, that should be fixed and still visible when the user scrolls right.

    zebra_stripes bool False

    Enables or disables a zebra effect applied to the background color of the rows of the table, where alternate colors are styled differently to improve the readability of the table.

    header_height int 1

    The height, in number of cells, of the data table header.

    show_cursor bool True

    Whether the cursor should be visible when navigating the data table or not.

    cursor_foreground_priority Literal['renderable', 'css'] 'css'

    If the data associated with a cell is an arbitrary renderable with a set foreground color, this determines whether that color is prioritized over the cursor component class or not.

    cursor_background_priority Literal['renderable', 'css'] 'renderable'

    If the data associated with a cell is an arbitrary renderable with a set background color, this determines whether that color is prioritized over the cursor component class or not.

    cursor_type CursorType 'cell'

    The type of cursor to be used when navigating the data table with the keyboard.

    cell_padding int _DEFAULT_CELL_X_PADDING

    The number of cells added on each side of each column. Setting this value to zero will likely make your table very heard to read.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"Cursor Right\", show=False\n    ),\n    Binding(\n        \"left\", \"cursor_left\", \"Cursor Left\", show=False\n    ),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"datatable--cursor\",\n    \"datatable--hover\",\n    \"datatable--fixed\",\n    \"datatable--fixed-cursor\",\n    \"datatable--header\",\n    \"datatable--header-cursor\",\n    \"datatable--header-hover\",\n    \"datatable--odd-row\",\n    \"datatable--even-row\",\n}\n
    Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cell_padding","title":"cell_padding instance-attribute class-attribute","text":"
    cell_padding = cell_padding\n

    Horizontal padding between cells, applied on each side of each cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.columns","title":"columns instance-attribute","text":"
    columns: dict[ColumnKey, Column] = {}\n

    Metadata about the columns of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_background_priority","title":"cursor_background_priority instance-attribute","text":"
    cursor_background_priority = cursor_background_priority\n

    Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_column","title":"cursor_column property","text":"
    cursor_column: int\n

    The index of the column that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_coordinate","title":"cursor_coordinate instance-attribute class-attribute","text":"
    cursor_coordinate: Reactive[Coordinate] = Reactive(\n    Coordinate(0, 0), repaint=False, always_update=True\n)\n

    Current cursor Coordinate.

    This can be set programmatically or changed via the method move_cursor.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_foreground_priority","title":"cursor_foreground_priority instance-attribute","text":"
    cursor_foreground_priority = cursor_foreground_priority\n

    Should we prioritize the cursor component class CSS foreground or the renderable foreground in the event where a cell contains a renderable with a foreground color.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_row","title":"cursor_row property","text":"
    cursor_row: int\n

    The index of the row that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_type","title":"cursor_type instance-attribute class-attribute","text":"
    cursor_type: Reactive[CursorType] = cursor_type\n

    The type of cursor of the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_columns","title":"fixed_columns instance-attribute class-attribute","text":"
    fixed_columns = fixed_columns\n

    The number of columns to fix (prevented from scrolling).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_rows","title":"fixed_rows instance-attribute class-attribute","text":"
    fixed_rows = fixed_rows\n

    The number of rows to fix (prevented from scrolling).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.header_height","title":"header_height instance-attribute class-attribute","text":"
    header_height = header_height\n

    The height of the header row (the row of column labels).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_column","title":"hover_column property","text":"
    hover_column: int\n

    The index of the column that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_coordinate","title":"hover_coordinate instance-attribute class-attribute","text":"
    hover_coordinate: Reactive[Coordinate] = Reactive(\n    Coordinate(0, 0), repaint=False, always_update=True\n)\n

    The coordinate of the DataTable that is being hovered.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_row","title":"hover_row property","text":"
    hover_row: int\n

    The index of the row that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_columns","title":"ordered_columns property","text":"
    ordered_columns: list[Column]\n

    The list of Columns in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_rows","title":"ordered_rows property","text":"
    ordered_rows: list[Row]\n

    The list of Rows in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.row_count","title":"row_count property","text":"
    row_count: int\n

    The number of rows currently present in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.rows","title":"rows instance-attribute","text":"
    rows: dict[RowKey, Row] = {}\n

    Metadata about the rows of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_cursor","title":"show_cursor instance-attribute class-attribute","text":"
    show_cursor = show_cursor\n

    Show/hide both the keyboard and hover cursor.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_header","title":"show_header instance-attribute class-attribute","text":"
    show_header = show_header\n

    Show/hide the header row (the row of column labels).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_row_labels","title":"show_row_labels instance-attribute class-attribute","text":"
    show_row_labels = show_row_labels\n

    Show/hide the column containing the labels of rows.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.zebra_stripes","title":"zebra_stripes instance-attribute class-attribute","text":"
    zebra_stripes = zebra_stripes\n

    Apply zebra effect on row backgrounds (light, dark, light, dark, ...).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted","title":"CellHighlighted class","text":"
    def __init__(self, data_table, value, coordinate, cell_key):\n

    Bases: Message

    Posted when the cursor moves to highlight a new cell.

    This is only relevant when the cursor_type is \"cell\". It's also posted when the cell cursor is re-enabled (by setting show_cursor=True), and when the cursor type is changed to \"cell\". Can be handled using on_data_table_cell_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
    cell_key: CellKey = cell_key\n

    The key for the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
    coordinate: Coordinate = coordinate\n

    The coordinate of the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
    value: CellType = value\n

    The value in the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected","title":"CellSelected class","text":"
    def __init__(self, data_table, value, coordinate, cell_key):\n

    Bases: Message

    Posted by the DataTable widget when a cell is selected.

    This is only relevant when the cursor_type is \"cell\". Can be handled using on_data_table_cell_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
    cell_key: CellKey = cell_key\n

    The key for the selected cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
    coordinate: Coordinate = coordinate\n

    The coordinate of the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.value","title":"value instance-attribute","text":"
    value: CellType = value\n

    The value in the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted","title":"ColumnHighlighted class","text":"
    def __init__(self, data_table, cursor_column, column_key):\n

    Bases: Message

    Posted when a column is highlighted.

    This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column: int = cursor_column\n

    The x-coordinate of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected","title":"ColumnSelected class","text":"
    def __init__(self, data_table, cursor_column, column_key):\n

    Bases: Message

    Posted when a column is selected.

    This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column: int = cursor_column\n

    The x-coordinate of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected","title":"HeaderSelected class","text":"
    def __init__(self, data_table, column_key, column_index, label):\n

    Bases: Message

    Posted when a column header/label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_index","title":"column_index instance-attribute","text":"
    column_index = column_index\n

    The index for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted","title":"RowHighlighted class","text":"
    def __init__(self, data_table, cursor_row, row_key):\n

    Bases: Message

    Posted when a row is highlighted.

    This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row: int = cursor_row\n

    The y-coordinate of the cursor that highlighted the row.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey = row_key\n

    The key of the row that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected","title":"RowLabelSelected class","text":"
    def __init__(self, data_table, row_key, row_index, label):\n

    Bases: Message

    Posted when a row label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_index","title":"row_index instance-attribute","text":"
    row_index = row_index\n

    The index for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected","title":"RowSelected class","text":"
    def __init__(self, data_table, cursor_row, row_key):\n

    Bases: Message

    Posted when a row is selected.

    This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row: int = cursor_row\n

    The y-coordinate of the cursor that made the selection.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey = row_key\n

    The key of the row that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the cursor one page down.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the cursor one page up.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_end","title":"action_scroll_end method","text":"
    def action_scroll_end(self):\n

    Scroll to the bottom of the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_home","title":"action_scroll_home method","text":"
    def action_scroll_home(self):\n

    Scroll to the top of the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_column","title":"add_column method","text":"
    def add_column(\n    self, label, *, width=None, key=None, default=None\n):\n

    Add a column to the table.

    Parameters Parameter Default Description label TextType required

    A str or Text object containing the label (shown top of column).

    width int | None None

    Width of the column in cells or None to fit content.

    key str | None None

    A key which uniquely identifies this column. If None, it will be generated for you.

    default CellType | None None

    The value to insert into pre-existing rows.

    Returns Type Description ColumnKey

    Uniquely identifies this column. Can be used to retrieve this column regardless of its current location in the DataTable (it could have moved after being added due to sorting/insertion/deletion of other columns).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_columns","title":"add_columns method","text":"
    def add_columns(self, *labels):\n

    Add a number of columns.

    Parameters Parameter Default Description *labels TextType ()

    Column headers.

    Returns Type Description list[ColumnKey]

    A list of the keys for the columns that were added. See the add_column method docstring for more information on how these keys are used.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_row","title":"add_row method","text":"
    def add_row(self, *cells, height=1, key=None, label=None):\n

    Add a row at the bottom of the DataTable.

    Parameters Parameter Default Description *cells CellType ()

    Positional arguments should contain cell data.

    height int | None 1

    The height of a row (in lines). Use None to auto-detect the optimal height.

    key str | None None

    A key which uniquely identifies this row. If None, it will be generated for you and returned.

    label TextType | None None

    The label for the row. Will be displayed to the left if supplied.

    Returns Type Description RowKey

    Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_rows","title":"add_rows method","text":"
    def add_rows(self, rows):\n

    Add a number of rows at the bottom of the DataTable.

    Parameters Parameter Default Description rows Iterable[Iterable[CellType]] required

    Iterable of rows. A row is an iterable of cells.

    Returns Type Description list[RowKey]

    A list of the keys for the rows that were added. See the add_row method docstring for more information on how these keys are used.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.clear","title":"clear method","text":"
    def clear(self, columns=False):\n

    Clear the table.

    Parameters Parameter Default Description columns bool False

    Also clear the columns.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key method","text":"
    def coordinate_to_cell_key(self, coordinate):\n

    Return the key for the cell currently occupying this coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to exam the current cell key of.

    Returns Type Description CellKey

    The key of the cell currently occupying this coordinate.

    Raises Type Description CellDoesNotExist

    If the coordinate is not valid.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell","title":"get_cell method","text":"
    def get_cell(self, row_key, column_key):\n

    Given a row key and column key, return the value of the corresponding cell.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key of the cell.

    column_key ColumnKey | str required

    The column key of the cell.

    Returns Type Description CellType

    The value of the cell identified by the row and column keys.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_at","title":"get_cell_at method","text":"
    def get_cell_at(self, coordinate):\n

    Get the value from the cell occupying the given coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to retrieve the value from.

    Returns Type Description CellType

    The value of the cell at the coordinate.

    Raises Type Description CellDoesNotExist

    If there is no cell with the given coordinate.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_coordinate","title":"get_cell_coordinate method","text":"
    def get_cell_coordinate(self, row_key, column_key):\n

    Given a row key and column key, return the corresponding cell coordinate.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key of the cell.

    column_key Column | str required

    The column key of the cell.

    Returns Type Description Coordinate

    The current coordinate of the cell identified by the row and column keys.

    Raises Type Description CellDoesNotExist

    If the specified cell does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column","title":"get_column method","text":"
    def get_column(self, column_key):\n

    Get the values from the column identified by the given column key.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The key of the column.

    Returns Type Description Iterable[CellType]

    A generator which yields the cells in the column.

    Raises Type Description ColumnDoesNotExist

    If there is no column corresponding to the key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_at","title":"get_column_at method","text":"
    def get_column_at(self, column_index):\n

    Get the values from the column at a given index.

    Parameters Parameter Default Description column_index int required

    The index of the column.

    Returns Type Description Iterable[CellType]

    A generator which yields the cells in the column.

    Raises Type Description ColumnDoesNotExist

    If there is no column with the given index.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_index","title":"get_column_index method","text":"
    def get_column_index(self, column_key):\n

    Return the current index for the column identified by column_key.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The column key to find the current index of.

    Returns Type Description int

    The current index of the specified column key.

    Raises Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row","title":"get_row method","text":"
    def get_row(self, row_key):\n

    Get the values from the row identified by the given row key.

    Parameters Parameter Default Description row_key RowKey | str required

    The key of the row.

    Returns Type Description list[CellType]

    A list of the values contained within the row.

    Raises Type Description RowDoesNotExist

    When there is no row corresponding to the key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_at","title":"get_row_at method","text":"
    def get_row_at(self, row_index):\n

    Get the values from the cells in a row at a given index. This will return the values from a row based on the rows current position in the table.

    Parameters Parameter Default Description row_index int required

    The index of the row.

    Returns Type Description list[CellType]

    A list of the values contained in the row.

    Raises Type Description RowDoesNotExist

    If there is no row with the given index.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_height","title":"get_row_height method","text":"
    def get_row_height(self, row_key):\n

    Given a row key, return the height of that row in terminal cells.

    Parameters Parameter Default Description row_key RowKey required

    The key of the row.

    Returns Type Description int

    The height of the row, measured in terminal character cells.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_index","title":"get_row_index method","text":"
    def get_row_index(self, row_key):\n

    Return the current index for the row identified by row_key.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key to find the current index of.

    Returns Type Description int

    The current index of the specified row key.

    Raises Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_column_index","title":"is_valid_column_index method","text":"
    def is_valid_column_index(self, column_index):\n

    Return a boolean indicating whether the column_index is within table bounds.

    Parameters Parameter Default Description column_index int required

    The column index to check.

    Returns Type Description bool

    True if the column index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_coordinate","title":"is_valid_coordinate method","text":"
    def is_valid_coordinate(self, coordinate):\n

    Return a boolean indicating whether the given coordinate is valid.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to validate.

    Returns Type Description bool

    True if the coordinate is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_row_index","title":"is_valid_row_index method","text":"
    def is_valid_row_index(self, row_index):\n

    Return a boolean indicating whether the row_index is within table bounds.

    Parameters Parameter Default Description row_index int required

    The row index to check.

    Returns Type Description bool

    True if the row index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.move_cursor","title":"move_cursor method","text":"
    def move_cursor(self, *, row=None, column=None, animate=False):\n

    Move the cursor to the given position.

    Example
    datatable = app.query_one(DataTable)\ndatatable.move_cursor(row=4, column=6)\n# datatable.cursor_coordinate == Coordinate(4, 6)\ndatatable.move_cursor(row=3)\n# datatable.cursor_coordinate == Coordinate(3, 6)\n
    Parameters Parameter Default Description row int | None None

    The new row to move the cursor to.

    column int | None None

    The new column to move the cursor to.

    animate bool False

    Whether to animate the change of coordinates.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_column","title":"refresh_column method","text":"
    def refresh_column(self, column_index):\n

    Refresh the column at the given index.

    Parameters Parameter Default Description column_index int required

    The index of the column to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_coordinate","title":"refresh_coordinate method","text":"
    def refresh_coordinate(self, coordinate):\n

    Refresh the cell at a coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_row","title":"refresh_row method","text":"
    def refresh_row(self, row_index):\n

    Refresh the row at the given index.

    Parameters Parameter Default Description row_index int required

    The index of the row to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_column","title":"remove_column method","text":"
    def remove_column(self, column_key):\n

    Remove a column (identified by a key) from the DataTable.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The key identifying the column to remove.

    Raises Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_row","title":"remove_row method","text":"
    def remove_row(self, row_key):\n

    Remove a row (identified by a key) from the DataTable.

    Parameters Parameter Default Description row_key RowKey | str required

    The key identifying the row to remove.

    Raises Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.sort","title":"sort method","text":"
    def sort(self, *columns, key=None, reverse=False):\n

    Sort the rows in the DataTable by one or more column keys or a key function (or other callable). If both columns and a key function are specified, only data from those columns will sent to the key function.

    Parameters Parameter Default Description columns ColumnKey | str ()

    One or more columns to sort by the values in.

    key Callable[[Any], Any] | None None

    A function (or other callable) that returns a key to use for sorting purposes.

    reverse bool False

    If True, the sort order will be reversed.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell","title":"update_cell method","text":"
    def update_cell(\n    self, row_key, column_key, value, *, update_width=False\n):\n

    Update the cell identified by the specified row key and column key.

    Parameters Parameter Default Description row_key RowKey | str required

    The key identifying the row.

    column_key ColumnKey | str required

    The key identifying the column.

    value CellType required

    The new value to put inside the cell.

    update_width bool False

    Whether to resize the column width to accommodate for the new cell content.

    Raises Type Description CellDoesNotExist

    When the supplied row_key and column_key cannot be found in the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell_at","title":"update_cell_at method","text":"
    def update_cell_at(\n    self, coordinate, value, *, update_width=False\n):\n

    Update the content inside the cell currently occupying the given coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to update the cell at.

    value CellType required

    The new value to place inside the cell.

    update_width bool False

    Whether to resize the column width to accommodate for the new cell content.

    "},{"location":"widgets/data_table/#textual.widgets.data_table","title":"textual.widgets.data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.CellType","title":"CellType module-attribute","text":"
    CellType = TypeVar('CellType')\n

    Type used for cells in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CursorType","title":"CursorType module-attribute","text":"
    CursorType = Literal['cell', 'row', 'column', 'none']\n

    The valid types of cursors for DataTable.cursor_type.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CellDoesNotExist","title":"CellDoesNotExist class","text":"

    Bases: Exception

    The cell key/index was invalid.

    Raised when the coordinates or cell key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CellKey","title":"CellKey class","text":"

    Bases: NamedTuple

    A unique identifier for a cell in the DataTable.

    A cell key is a (row_key, column_key) tuple.

    Even if the cell changes visual location (i.e. moves to a different coordinate in the table), this key can still be used to retrieve it, regardless of where it currently is.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.CellKey.column_key","title":"column_key instance-attribute","text":"
    column_key: ColumnKey\n

    The key of this cell's column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.CellKey.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey\n

    The key of this cell's row.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column class","text":"

    Metadata for a column in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.Column.get_render_width","title":"get_render_width method","text":"
    def get_render_width(self, data_table):\n

    Width, in cells, required to render the column with padding included.

    Parameters Parameter Default Description data_table DataTable[Any] required

    The data table where the column will be rendered.

    Returns Type Description int

    The width, in cells, required to render the column with padding included.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist class","text":"

    Bases: Exception

    Raised when the column index or column key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnKey","title":"ColumnKey class","text":"

    Bases: StringKey

    Uniquely identifies a column in the DataTable.

    Even if the visual location of the column changes due to sorting or other modifications, a key will always refer to the same column.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.DuplicateKey","title":"DuplicateKey class","text":"

    Bases: Exception

    The key supplied already exists.

    Raised when the RowKey or ColumnKey provided already refers to an existing row or column in the DataTable. Keys must be unique.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Row","title":"Row class","text":"

    Metadata for a row in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist class","text":"

    Bases: Exception

    Raised when the row index or row key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowKey","title":"RowKey class","text":"

    Bases: StringKey

    Uniquely identifies a row in the DataTable.

    Even if the visual location of the row changes due to sorting or other modifications, a key will always refer to the same row.

    "},{"location":"widgets/digits/","title":"Digits","text":"

    Added in version 0.33.0

    A widget to display numerical values in tall multi-line characters.

    The digits 0-9 are supported, in addition to the following characters +, -, ^, :, and \u00d7. Other characters will be displayed in a regular size font.

    You can set the text to be displayed in the constructor, or call update() to change the text after the widget has been mounted.

    This widget will respect the text-align rule.

    • Focusable
    • Container
    "},{"location":"widgets/digits/#example","title":"Example","text":"

    The following example displays a few digits of Pi:

    Outputdigits.py

    DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

    from textual.app import App, ComposeResult\nfrom textual.widgets import Digits\n\n\nclass DigitApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #pi {\n        border: double green;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Digits(\"3.141,592,653,5897\", id=\"pi\")\n\n\nif __name__ == \"__main__\":\n    app = DigitApp()\n    app.run()\n

    Here's another example which uses Digits to display the current time:

    Outputclock.py

    ClockApp \u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u257a\u2501\u2513\u250f\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513 \u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u00a0\u2501\u252b\u2517\u2501\u252b\u00a0:\u00a0\u2517\u2501\u2513\u2523\u2501\u252b \u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u257a\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u00a0\u257a\u2501\u251b\u2517\u2501\u251b

    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Digits\n\n\nclass ClockApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #clock {\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Digits(\"\", id=\"clock\")\n\n    def on_ready(self) -> None:\n        self.update_clock()\n        self.set_interval(1, self.update_clock)\n\n    def update_clock(self) -> None:\n        clock = datetime.now().time()\n        self.query_one(Digits).update(f\"{clock:%T}\")\n\n\nif __name__ == \"__main__\":\n    app = ClockApp()\n    app.run()\n
    "},{"location":"widgets/digits/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/digits/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/digits/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/digits/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/digits/#textual.widgets.Digits","title":"textual.widgets.Digits class","text":"
    def __init__(\n    self,\n    value=\"\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A widget to display numerical values using a 3x3 grid of unicode characters.

    name: The name of the widget.\nid: The ID of the widget in the DOM.\nclasses: The CSS classes of the widget.\ndisabled: Whether the widget is disabled or not.\n
    "},{"location":"widgets/digits/#textual.widgets._digits.Digits.value","title":"value property","text":"
    value: str\n

    The current value displayed in the Digits.

    "},{"location":"widgets/digits/#textual.widgets._digits.Digits.update","title":"update method","text":"
    def update(self, value):\n

    Update the Digits with a new value.

    Parameters Parameter Default Description value str required

    New value to display.

    Raises Type Description ValueError

    If the value isn't a str.

    "},{"location":"widgets/directory_tree/","title":"DirectoryTree","text":"

    A tree control to navigate the contents of your filesystem.

    • Focusable
    • Container
    "},{"location":"widgets/directory_tree/#example","title":"Example","text":"

    The example below creates a simple tree to navigate the current working directory.

    from textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\n\n\nclass DirectoryTreeApp(App):\n    def compose(self) -> ComposeResult:\n        yield DirectoryTree(\"./\")\n\n\nif __name__ == \"__main__\":\n    app = DirectoryTreeApp()\n    app.run()\n
    "},{"location":"widgets/directory_tree/#filtering","title":"Filtering","text":"

    There may be times where you want to filter what appears in the DirectoryTree. To do this inherit from DirectoryTree and implement your own version of the filter_paths method. It should take an iterable of Python Path objects, and return those that pass the filter. For example, if you wanted to take the above code an filter out all of the \"hidden\" files and directories:

    Outputdirectory_tree_filtered.py

    DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CHANGELOG.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CODE_OF_CONDUCT.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CONTRIBUTING.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0docs.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0faq.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0keys.log \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0LICENSE \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0Makefile \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-common.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-nav-offline.yml

    from pathlib import Path\nfrom typing import Iterable\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\n\n\nclass FilteredDirectoryTree(DirectoryTree):\n    def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:\n        return [path for path in paths if not path.name.startswith(\".\")]\n\n\nclass DirectoryTreeApp(App):\n    def compose(self) -> ComposeResult:\n        yield FilteredDirectoryTree(\"./\")\n\n\nif __name__ == \"__main__\":\n    app = DirectoryTreeApp()\n    app.run()\n
    "},{"location":"widgets/directory_tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/directory_tree/#messages","title":"Messages","text":"
    • DirectoryTree.FileSelected
    "},{"location":"widgets/directory_tree/#bindings","title":"Bindings","text":"

    The directory tree widget inherits the bindings from the tree widget.

    "},{"location":"widgets/directory_tree/#component-classes","title":"Component Classes","text":"

    The directory tree widget provides the following component classes:

    Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

    See also the component classes for Tree.

    "},{"location":"widgets/directory_tree/#see-also","title":"See Also","text":"
    • Tree code reference
    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree","title":"textual.widgets.DirectoryTree class","text":"
    def __init__(\n    self,\n    path,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Tree[DirEntry]

    A Tree widget that presents files and directories.

    Parameters Parameter Default Description path str | Path required

    Path to directory.

    name str | None None

    The name of the widget, or None for no name.

    id str | None None

    The ID of the widget in the DOM, or None for no ID.

    classes str | None None

    A space-separated list of classes, or None for no classes.

    disabled bool False

    Whether the directory tree is disabled or not.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"directory-tree--extension\",\n    \"directory-tree--file\",\n    \"directory-tree--folder\",\n    \"directory-tree--hidden\",\n}\n
    Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

    See also the component classes for Tree.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.PATH","title":"PATH instance-attribute class-attribute","text":"
    PATH: Callable[[str | Path], Path] = Path\n

    Callable that returns a fresh path object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.path","title":"path instance-attribute class-attribute","text":"
    path: var[str | Path] = path\n

    The path that is the root of the directory tree.

    Note

    This can be set to either a str or a pathlib.Path object, but the value will always be a pathlib.Path object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected","title":"DirectorySelected class","text":"
    def __init__(self, node, path):\n

    Bases: Message

    Posted when a directory is selected.

    Can be handled using on_directory_tree_directory_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The tree node for the directory that was selected.

    path Path required

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.control","title":"control property","text":"
    control: Tree[DirEntry]\n

    The Tree that had a directory selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
    node: TreeNode[DirEntry] = node\n

    The tree node of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
    path: Path = path\n

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected","title":"FileSelected class","text":"
    def __init__(self, node, path):\n

    Bases: Message

    Posted when a file is selected.

    Can be handled using on_directory_tree_file_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The tree node for the file that was selected.

    path Path required

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.control","title":"control property","text":"
    control: Tree[DirEntry]\n

    The Tree that had a file selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
    node: TreeNode[DirEntry] = node\n

    The tree node of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
    path: Path = path\n

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.clear_node","title":"clear_node method","text":"
    def clear_node(self, node):\n

    Clear all nodes under the given node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.filter_paths","title":"filter_paths method","text":"
    def filter_paths(self, paths):\n

    Filter the paths before adding them to the tree.

    Parameters Parameter Default Description paths Iterable[Path] required

    The paths to be filtered.

    Returns Type Description Iterable[Path]

    The filtered paths.

    By default this method returns all of the paths provided. To create a filtered DirectoryTree inherit from it and implement your own version of this method.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.process_label","title":"process_label method","text":"
    def process_label(self, label):\n

    Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered.

    Parameters Parameter Default Description label TextType required

    Label.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload","title":"reload method","text":"
    def reload(self):\n

    Reload the DirectoryTree contents.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload_node","title":"reload_node method","text":"
    def reload_node(self, node):\n

    Reload the given node's contents.

    The return value may be awaited to ensure the DirectoryTree has reached a stable state and is no longer performing any node reloading (of this node or any other nodes).

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The node to reload.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.render_label","title":"render_label method","text":"
    def render_label(self, node, base_style, style):\n

    Render a label for the given node.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    A tree node.

    base_style Style required

    The base style of the widget.

    style Style required

    The additional style for the label.

    Returns Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reset_node","title":"reset_node method","text":"
    def reset_node(self, node, label, data=None):\n

    Clear the subtree and reset the given node.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The node to reset.

    label TextType required

    The label for the node.

    data DirEntry | None None

    Optional data for the node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.validate_path","title":"validate_path method","text":"
    def validate_path(self, path):\n

    Ensure that the path is of the Path type.

    Parameters Parameter Default Description path str | Path required

    The path to validate.

    Returns Type Description Path

    The validated Path value.

    Note

    The result will always be a Python Path object, regardless of the value given.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.watch_path","title":"watch_path async","text":"
    def watch_path(self):\n

    Watch for changes to the path of the directory tree.

    If the path is changed the directory tree will be repopulated using the new value as the root.

    "},{"location":"widgets/footer/","title":"Footer","text":"

    A simple footer widget which is docked to the bottom of its parent container. Displays available keybindings for the currently focused widget.

    • Focusable
    • Container
    "},{"location":"widgets/footer/#example","title":"Example","text":"

    The example below shows an app with a single keybinding that contains only a Footer widget. Notice how the Footer automatically displays the keybinding.

    Outputfooter.py

    FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

    from textual.app import App, ComposeResult\nfrom textual.binding import Binding\nfrom textual.widgets import Footer\n\n\nclass FooterApp(App):\n    BINDINGS = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit the app\"),\n        Binding(\n            key=\"question_mark\",\n            action=\"help\",\n            description=\"Show help screen\",\n            key_display=\"?\",\n        ),\n        Binding(key=\"delete\", action=\"delete\", description=\"Delete the thing\"),\n        Binding(key=\"j\", action=\"down\", description=\"Scroll down\", show=False),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = FooterApp()\n    app.run()\n
    "},{"location":"widgets/footer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight_key str None Stores the currently highlighted key. This is typically the key the cursor is hovered over in the footer."},{"location":"widgets/footer/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/footer/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/footer/#component-classes","title":"Component Classes","text":"

    The footer widget provides the following component classes:

    Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#additional-notes","title":"Additional Notes","text":"
    • You can prevent keybindings from appearing in the footer by setting the show argument of the Binding to False.
    • You can customize the text that appears for the key itself in the footer using the key_display argument of Binding.
    "},{"location":"widgets/footer/#textual.widgets.Footer","title":"textual.widgets.Footer class","text":"
    def __init__(self):\n

    Bases: Widget

    A simple footer widget which docks itself to the bottom of the parent container.

    "},{"location":"widgets/footer/#textual.widgets._footer.Footer.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"footer--description\",\n    \"footer--key\",\n    \"footer--highlight\",\n    \"footer--highlight-key\",\n}\n
    Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#textual.widgets._footer.Footer.watch_highlight_key","title":"watch_highlight_key async","text":"
    def watch_highlight_key(self):\n

    If highlight key changes we need to regenerate the text.

    "},{"location":"widgets/header/","title":"Header","text":"

    A simple header widget which docks itself to the top of the parent container.

    Note

    The application title which is shown in the header is taken from the title and sub_title of the application.

    • Focusable
    • Container
    "},{"location":"widgets/header/#example","title":"Example","text":"

    The example below shows an app with a Header.

    Outputheader.py

    HeaderApp \u2b58HeaderApp

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header\n\n\nclass HeaderApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n\n\nif __name__ == \"__main__\":\n    app = HeaderApp()\n    app.run()\n

    This example shows how to set the text in the Header using App.title and App.sub_title:

    Outputheader_app_title.py

    HeaderApp \u2b58Header\u00a0Application\u00a0\u2014\u00a0With\u00a0title\u00a0and\u00a0sub-title

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header\n\n\nclass HeaderApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n\n    def on_mount(self) -> None:\n        self.title = \"Header Application\"\n        self.sub_title = \"With title and sub-title\"\n\n\nif __name__ == \"__main__\":\n    app = HeaderApp()\n    app.run()\n
    "},{"location":"widgets/header/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description tall bool True Whether the Header widget is displayed as tall or not. The tall variant is 3 cells tall by default. The non-tall variant is a single cell tall. This can be toggled by clicking on the header."},{"location":"widgets/header/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/header/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/header/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/header/#textual.widgets.Header","title":"textual.widgets.Header class","text":"
    def __init__(\n    self,\n    show_clock=False,\n    *,\n    name=None,\n    id=None,\n    classes=None\n):\n

    Bases: Widget

    A header widget with icon and clock.

    Parameters Parameter Default Description show_clock bool False

    True if the clock should be shown on the right of the header.

    name str | None None

    The name of the header widget.

    id str | None None

    The ID of the header widget in the DOM.

    classes str | None None

    The CSS classes of the header widget.

    "},{"location":"widgets/header/#textual.widgets._header.Header.screen_sub_title","title":"screen_sub_title property","text":"
    screen_sub_title: str\n

    The sub-title that this header will display.

    This depends on Screen.sub_title and App.sub_title.

    "},{"location":"widgets/header/#textual.widgets._header.Header.screen_title","title":"screen_title property","text":"
    screen_title: str\n

    The title that this header will display.

    This depends on Screen.title and App.title.

    "},{"location":"widgets/header/#textual.widgets._header.Header.tall","title":"tall instance-attribute class-attribute","text":"
    tall: Reactive[bool] = Reactive(False)\n

    Set to True for a taller header or False for a single line header.

    "},{"location":"widgets/input/","title":"Input","text":"

    A single-line text input widget.

    • Focusable
    • Container
    "},{"location":"widgets/input/#examples","title":"Examples","text":""},{"location":"widgets/input/#a-simple-example","title":"A Simple Example","text":"

    The example below shows how you might create a simple form using two Input widgets.

    Outputinput.py

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Input\n\n\nclass InputApp(App):\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"First Name\")\n        yield Input(placeholder=\"Last Name\")\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n
    "},{"location":"widgets/input/#input-types","title":"Input Types","text":"

    The Input widget supports a type parameter which will prevent the user from typing invalid characters. You can set type to any of the following values:

    input.type Description \"integer\" Restricts input to integers. \"number\" Restricts input to a floating point number. \"text\" Allow all text (no restrictions). Outputinput_types.py

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAn\u00a0integer\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aA\u00a0number\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Input\n\n\nclass InputApp(App):\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"An integer\", type=\"integer\")\n        yield Input(placeholder=\"A number\", type=\"number\")\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    If you set type to something other than \"text\", then the Input will apply the appropriate validator.

    "},{"location":"widgets/input/#restricting-input","title":"Restricting Input","text":"

    You can limit input to particular characters by supplying the restrict parameter, which should be a regular expression. The Input widget will prevent the addition of any characters that would cause the regex to no longer match. For instance, if you wanted to limit characters to binary you could set restrict=r\"[01]*\".

    Note

    The restrict regular expression is applied to the full value and not just to the new character.

    "},{"location":"widgets/input/#maximum-length","title":"Maximum Length","text":"

    You can limit the length of the input by setting max_length to a value greater than zero. This will prevent the user from typing any more characters when the maximum has been reached.

    "},{"location":"widgets/input/#validating-input","title":"Validating Input","text":"

    You can supply one or more validators to the Input widget to validate the value.

    All the supplied validators will run when the value changes, the Input is submitted, or focus moves out of the Input. The values \"changed\", \"submitted\", and \"blur\", can be passed as an iterable to the Input parameter validate_on to request that validation occur only on the respective mesages. (See InputValidationOn and Input.validate_on.) For example, the code below creates an Input widget that only gets validated when the value is submitted explicitly:

    input = Input(validate_on=[\"submitted\"])\n

    Validation is considered to have failed if any of the validators fail.

    You can check whether the validation succeeded or failed inside an Input.Changed or Input.Submitted handler by looking at the validation_result attribute on these events.

    In the example below, we show how to combine multiple validators and update the UI to tell the user why validation failed. Click the tabs to see the output for validation failures and successes.

    input_validation.pyValidation FailureValidation Success
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.validation import Function, Number, ValidationResult, Validator\nfrom textual.widgets import Input, Label, Pretty\n\n\nclass InputApp(App):\n    # (6)!\n    CSS = \"\"\"\n    Input.-valid {\n        border: tall $success 60%;\n    }\n    Input.-valid:focus {\n        border: tall $success;\n    }\n    Input {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    Pretty {\n        margin: 1 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Enter an even number between 1 and 100 that is also a palindrome.\")\n        yield Input(\n            placeholder=\"Enter a number...\",\n            validators=[\n                Number(minimum=1, maximum=100),  # (1)!\n                Function(is_even, \"Value is not even.\"),  # (2)!\n                Palindrome(),  # (3)!\n            ],\n        )\n        yield Pretty([])\n\n    @on(Input.Changed)\n    def show_invalid_reasons(self, event: Input.Changed) -> None:\n        # Updating the UI to show the reasons why validation failed\n        if not event.validation_result.is_valid:  # (4)!\n            self.query_one(Pretty).update(event.validation_result.failure_descriptions)\n        else:\n            self.query_one(Pretty).update([])\n\n\ndef is_even(value: str) -> bool:\n    try:\n        return int(value) % 2 == 0\n    except ValueError:\n        return False\n\n\n# A custom validator\nclass Palindrome(Validator):  # (5)!\n    def validate(self, value: str) -> ValidationResult:\n        \"\"\"Check a string is equal to its reverse.\"\"\"\n        if self.is_palindrome(value):\n            return self.success()\n        else:\n            return self.failure(\"That's not a palindrome :/\")\n\n    @staticmethod\n    def is_palindrome(value: str) -> bool:\n        return value == value[::-1]\n\n\napp = InputApp()\n\nif __name__ == \"__main__\":\n    app.run()\n
    1. Number is a built-in Validator. It checks that the value in the Input is a valid number, and optionally can check that it falls within a range.
    2. Function lets you quickly define custom validation constraints. In this case, we check the value in the Input is even.
    3. Palindrome is a custom Validator defined below.
    4. The Input.Changed event has a validation_result attribute which contains information about the validation that occurred when the value changed.
    5. Here's how we can implement a custom validator which checks if a string is a palindrome. Note how the description passed into self.failure corresponds to the message seen on UI.
    6. Textual offers default styling for the -invalid CSS class (a red border), which is automatically applied to Input when validation fails. We can also provide custom styling for the -valid class, as seen here. In this case, we add a green border around the Input to indicate successful validation.

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a-23\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e [ 'Must\u00a0be\u00a0between\u00a01\u00a0and\u00a0100.', 'Value\u00a0is\u00a0not\u00a0even.', \"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\" ]

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a44\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e []

    Textual offers several built-in validators for common requirements, but you can easily roll your own by extending Validator, as seen for Palindrome in the example above.

    "},{"location":"widgets/input/#validate-empty","title":"Validate Empty","text":"

    If you set valid_empty=True then empty values will bypass any validators, and empty values will be considered valid.

    "},{"location":"widgets/input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description cursor_blink bool True True if cursor blinking is enabled. value str \"\" The value currently in the text input. cursor_position int 0 The index of the cursor in the value string. placeholder str \"\" The dimmed placeholder text to display when the input is empty. password bool False True if the input should be masked. restrict str None Optional regular expression to restrict input. type str \"text\" The type of the input. max_length int None Maximum length of the input value. valid_empty bool False Allow empty values to bypass validation."},{"location":"widgets/input/#messages","title":"Messages","text":"
    • Input.Changed
    • Input.Submitted
    "},{"location":"widgets/input/#bindings","title":"Bindings","text":"

    The input widget defines the following bindings:

    Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#component-classes","title":"Component Classes","text":"

    The input widget provides the following component classes:

    Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#additional-notes","title":"Additional Notes","text":"
    • The spacing around the text content is due to border. To remove it, set border: none; in your CSS.
    "},{"location":"widgets/input/#textual.widgets.Input","title":"textual.widgets.Input class","text":"
    def __init__(\n    self,\n    value=None,\n    placeholder=\"\",\n    highlighter=None,\n    password=False,\n    *,\n    restrict=None,\n    type=\"text\",\n    max_length=0,\n    suggester=None,\n    validators=None,\n    validate_on=None,\n    valid_empty=False,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A text input widget.

    Parameters Parameter Default Description value str | None None

    An optional default value for the input.

    placeholder str ''

    Optional placeholder text for the input.

    highlighter Highlighter | None None

    An optional highlighter for the input.

    password bool False

    Flag to say if the field should obfuscate its content.

    restrict str | None None

    A regex to restrict character inputs.

    type InputType 'text'

    The type of the input.

    max_length int 0

    The maximum length of the input, or 0 for no maximum length.

    suggester Suggester | None None

    Suggester associated with this input instance.

    validators Validator | Iterable[Validator] | None None

    An iterable of validators that the Input value will be checked against.

    validate_on Iterable[InputValidationOn] | None None

    Zero or more of the values \"blur\", \"changed\", and \"submitted\", which determine when to do input validation. The default is to do validation for all messages.

    valid_empty bool False

    Empty values are valid.

    name str | None None

    Optional name for the input widget.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the input is disabled or not.

    "},{"location":"widgets/input/#textual.widgets._input.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"left\", \"cursor_left\", \"cursor left\", show=False\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_left_word\",\n        \"cursor left word\",\n        show=False,\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"cursor right\", show=False\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_right_word\",\n        \"cursor right word\",\n        show=False,\n    ),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"delete left\",\n        show=False,\n    ),\n    Binding(\"home,ctrl+a\", \"home\", \"home\", show=False),\n    Binding(\"end,ctrl+e\", \"end\", \"end\", show=False),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"delete right\",\n        show=False,\n    ),\n    Binding(\"enter\", \"submit\", \"submit\", show=False),\n    Binding(\n        \"ctrl+w\",\n        \"delete_left_word\",\n        \"delete left to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+u\",\n        \"delete_left_all\",\n        \"delete all to the left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+f\",\n        \"delete_right_word\",\n        \"delete right to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+k\",\n        \"delete_right_all\",\n        \"delete all to the right\",\n        show=False,\n    ),\n]\n
    Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#textual.widgets._input.Input.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"input--cursor\",\n    \"input--placeholder\",\n    \"input--suggestion\",\n}\n
    Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#textual.widgets._input.Input.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset: Offset\n

    The offset of the cursor of this input in screen-space. (x, y)/(column, row)

    "},{"location":"widgets/input/#textual.widgets._input.Input.cursor_width","title":"cursor_width property","text":"
    cursor_width: int\n

    The width of the input (with extra space for cursor at the end).

    "},{"location":"widgets/input/#textual.widgets._input.Input.is_valid","title":"is_valid property","text":"
    is_valid: bool\n

    Check if the value has passed validation.

    "},{"location":"widgets/input/#textual.widgets._input.Input.max_length","title":"max_length instance-attribute class-attribute","text":"
    max_length = max_length\n

    The maximum length of the input, in characters.

    "},{"location":"widgets/input/#textual.widgets._input.Input.restrict","title":"restrict instance-attribute class-attribute","text":"
    restrict = restrict\n

    A regular expression to limit changes in value.

    "},{"location":"widgets/input/#textual.widgets._input.Input.suggester","title":"suggester instance-attribute","text":"
    suggester: Suggester | None = suggester\n

    The suggester used to provide completions as the user types.

    "},{"location":"widgets/input/#textual.widgets._input.Input.type","title":"type instance-attribute class-attribute","text":"
    type = type\n

    The type of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.valid_empty","title":"valid_empty instance-attribute class-attribute","text":"
    valid_empty = var(False)\n

    Empty values should pass validation.

    "},{"location":"widgets/input/#textual.widgets._input.Input.validate_on","title":"validate_on instance-attribute","text":"
    validate_on = (\n    set(validate_on) & _POSSIBLE_VALIDATE_ON_VALUES\n    if validate_on is not None\n    else _POSSIBLE_VALIDATE_ON_VALUES\n)\n

    Set with event names to do input validation on.

    Validation can only be performed on blur, on input changes and on input submission.

    Example

    This creates an Input widget that only gets validated when the value is submitted explicitly:

    input = Input(validate_on=[\"submitted\"])\n
    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed","title":"Changed class","text":"

    Bases: Message

    Posted when the value changes.

    Can be handled using on_input_changed in a subclass of Input or in a parent widget in the DOM.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.control","title":"control property","text":"
    control: Input\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.input","title":"input instance-attribute","text":"
    input: Input\n

    The Input widget that was changed.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.validation_result","title":"validation_result instance-attribute class-attribute","text":"
    validation_result: ValidationResult | None = None\n

    The result of validating the value (formed by combining the results from each validator), or None if validation was not performed (for example when no validators are specified in the Inputs init)

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.value","title":"value instance-attribute","text":"
    value: str\n

    The value that the input was changed to.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted","title":"Submitted class","text":"

    Bases: Message

    Posted when the enter key is pressed within an Input.

    Can be handled using on_input_submitted in a subclass of Input or in a parent widget in the DOM.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.control","title":"control property","text":"
    control: Input\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.input","title":"input instance-attribute","text":"
    input: Input\n

    The Input widget that is being submitted.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.validation_result","title":"validation_result instance-attribute class-attribute","text":"
    validation_result: ValidationResult | None = None\n

    The result of validating the value on submission, formed by combining the results for each validator. This value will be None if no validation was performed, which will be the case if no validators are supplied to the corresponding Input widget.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.value","title":"value instance-attribute","text":"
    value: str\n

    The value of the Input being submitted.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left","title":"action_cursor_left method","text":"
    def action_cursor_left(self):\n

    Move the cursor one position to the left.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left_word","title":"action_cursor_left_word method","text":"
    def action_cursor_left_word(self):\n

    Move the cursor left to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right","title":"action_cursor_right method","text":"
    def action_cursor_right(self):\n

    Accept an auto-completion or move the cursor one position to the right.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right_word","title":"action_cursor_right_word method","text":"
    def action_cursor_right_word(self):\n

    Move the cursor right to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left","title":"action_delete_left method","text":"
    def action_delete_left(self):\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_all","title":"action_delete_left_all method","text":"
    def action_delete_left_all(self):\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_word","title":"action_delete_left_word method","text":"
    def action_delete_left_word(self):\n

    Delete leftward of the cursor position to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right","title":"action_delete_right method","text":"
    def action_delete_right(self):\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_all","title":"action_delete_right_all method","text":"
    def action_delete_right_all(self):\n

    Delete the current character and all characters to the right of the cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_word","title":"action_delete_right_word method","text":"
    def action_delete_right_word(self):\n

    Delete the current character and all rightward to the start of the next word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_end","title":"action_end method","text":"
    def action_end(self):\n

    Move the cursor to the end of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_home","title":"action_home method","text":"
    def action_home(self):\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_submit","title":"action_submit async","text":"
    def action_submit(self):\n

    Handle a submit action.

    Normally triggered by the user pressing Enter. This may also run any validators.

    "},{"location":"widgets/input/#textual.widgets._input.Input.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.insert_text_at_cursor","title":"insert_text_at_cursor method","text":"
    def insert_text_at_cursor(self, text):\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters Parameter Default Description text str required

    New text to insert.

    "},{"location":"widgets/input/#textual.widgets._input.Input.restricted","title":"restricted method","text":"
    def restricted(self):\n

    Called when a character has been restricted.

    The default behavior is to play the system bell. You may want to override this method if you want to disable the bell or do something else entirely.

    "},{"location":"widgets/input/#textual.widgets._input.Input.validate","title":"validate method","text":"
    def validate(self, value):\n

    Run all the validators associated with this Input on the supplied value.

    Runs all validators, combines the result into one. If any of the validators failed, the combined result will be a failure. If no validators are present, None will be returned. This also sets the -invalid CSS class on the Input if the validation fails, and sets the -valid CSS class on the Input if the validation succeeds.

    Returns Type Description ValidationResult | None

    A ValidationResult indicating whether all validators succeeded or not. That is, if any validator fails, the result will be an unsuccessful validation.

    "},{"location":"widgets/label/","title":"Label","text":"

    Added in version 0.5.0

    A widget which displays static text, but which can also contain more complex Rich renderables.

    • Focusable
    • Container
    "},{"location":"widgets/label/#example","title":"Example","text":"

    The example below shows how you can use a Label widget to display some text.

    Outputlabel.py

    LabelApp Hello,\u00a0world!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass LabelApp(App):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, world!\")\n\n\nif __name__ == \"__main__\":\n    app = LabelApp()\n    app.run()\n
    "},{"location":"widgets/label/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/label/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/label/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/label/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/label/#textual.widgets.Label","title":"textual.widgets.Label class","text":"

    Bases: Static

    A simple label widget for displaying text-oriented renderables.

    "},{"location":"widgets/list_item/","title":"ListItem","text":"

    Added in version 0.6.0

    ListItem is the type of the elements in a ListView.

    • Focusable
    • Container
    "},{"location":"widgets/list_item/#example","title":"Example","text":"

    The example below shows an app with a simple ListView, consisting of multiple ListItems. The arrow keys can be used to navigate the list.

    Outputlist_view.py

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    "},{"location":"widgets/list_item/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted bool False True if this ListItem is highlighted"},{"location":"widgets/list_item/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/list_item/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/list_item/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/list_item/#textual.widgets.ListItem","title":"textual.widgets.ListItem class","text":"

    Bases: Widget

    A widget that is an item within a ListView.

    A ListItem is designed for use within a ListView, please see ListView's documentation for more details on use.

    "},{"location":"widgets/list_item/#textual.widgets._list_item.ListItem.highlighted","title":"highlighted instance-attribute class-attribute","text":"
    highlighted = reactive(False)\n

    Is this item highlighted?

    "},{"location":"widgets/list_view/","title":"ListView","text":"

    Added in version 0.6.0

    Displays a vertical list of ListItems which can be highlighted and selected. Supports keyboard navigation.

    • Focusable
    • Container
    "},{"location":"widgets/list_view/#example","title":"Example","text":"

    The example below shows an app with a simple ListView.

    Outputlist_view.pylist_view.tcss

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nListView {\n    width: 30;\n    height: auto;\n    margin: 2 2;\n}\n\nLabel {\n    padding: 1 2;\n}\n
    "},{"location":"widgets/list_view/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description index int 0 The currently highlighted index."},{"location":"widgets/list_view/#messages","title":"Messages","text":"
    • ListView.Highlighted
    • ListView.Selected
    "},{"location":"widgets/list_view/#bindings","title":"Bindings","text":"

    The list view widget defines the following bindings:

    Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/list_view/#textual.widgets.ListView","title":"textual.widgets.ListView class","text":"
    def __init__(\n    self,\n    *children,\n    initial_index=0,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: VerticalScroll

    A vertical list view widget.

    Displays a vertical list of ListItems which can be highlighted and selected using the mouse or keyboard.

    Attributes Name Type Description index

    The index in the list that's currently highlighted.

    Parameters Parameter Default Description *children ListItem ()

    The ListItems to display in the list.

    initial_index int | None 0

    The index that should be highlighted when the list is first mounted.

    name str | None None

    The name of the widget.

    id str | None None

    The unique ID of the widget used in CSS/query selection.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the ListView is disabled or not.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.highlighted_child","title":"highlighted_child property","text":"
    highlighted_child: ListItem | None\n

    The currently highlighted ListItem, or None if nothing is highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.index","title":"index instance-attribute class-attribute","text":"
    index = reactive[Optional[int]](0, always_update=True)\n

    The index of the currently highlighted item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted","title":"Highlighted class","text":"
    def __init__(self, list_view, item):\n

    Bases: Message

    Posted when the highlighted item changes.

    Highlighted item is controlled using up/down keys. Can be handled using on_list_view_highlighted in a subclass of ListView or in a parent widget in the DOM.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'item'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.control","title":"control property","text":"
    control: ListView\n

    The view that contains the item highlighted.

    This is an alias for Highlighted.list_view and is used by the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.item","title":"item instance-attribute","text":"
    item: ListItem | None = item\n

    The highlighted item, if there is one highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
    list_view: ListView = list_view\n

    The view that contains the item highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected","title":"Selected class","text":"
    def __init__(self, list_view, item):\n

    Bases: Message

    Posted when a list item is selected, e.g. when you press the enter key on it.

    Can be handled using on_list_view_selected in a subclass of ListView or in a parent widget in the DOM.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'item'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.control","title":"control property","text":"
    control: ListView\n

    The view that contains the item selected.

    This is an alias for Selected.list_view and is used by the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.item","title":"item instance-attribute","text":"
    item: ListItem = item\n

    The selected item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
    list_view: ListView = list_view\n

    The view that contains the item selected.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Highlight the next item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Highlight the previous item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_select_cursor","title":"action_select_cursor method","text":"
    def action_select_cursor(self):\n

    Select the current item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.append","title":"append method","text":"
    def append(self, item):\n

    Append a new ListItem to the end of the ListView.

    Parameters Parameter Default Description item ListItem required

    The ListItem to append.

    Returns Type Description AwaitMount

    An awaitable that yields control to the event loop until the DOM has been updated with the new child item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all items from the ListView.

    Returns Type Description AwaitRemove

    An awaitable that yields control to the event loop until the DOM has been updated to reflect all children being removed.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.extend","title":"extend method","text":"
    def extend(self, items):\n

    Append multiple new ListItems to the end of the ListView.

    Parameters Parameter Default Description items Iterable[ListItem] required

    The ListItems to append.

    Returns Type Description AwaitMount

    An awaitable that yields control to the event loop until the DOM has been updated with the new child items.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.validate_index","title":"validate_index method","text":"
    def validate_index(self, index):\n

    Clamp the index to the valid range, or set to None if there's nothing to highlight.

    Parameters Parameter Default Description index int | None required

    The index to clamp.

    Returns Type Description int | None

    The clamped index.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.watch_index","title":"watch_index method","text":"
    def watch_index(self, old_index, new_index):\n

    Updates the highlighting when the index changes.

    "},{"location":"widgets/loading_indicator/","title":"LoadingIndicator","text":"

    Added in version 0.15.0

    Displays pulsating dots to indicate when data is being loaded.

    • Focusable
    • Container
    "},{"location":"widgets/loading_indicator/#example","title":"Example","text":"

    Simple usage example:

    Outputloading_indicator.py

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    from textual.app import App, ComposeResult\nfrom textual.widgets import LoadingIndicator\n\n\nclass LoadingApp(App):\n    def compose(self) -> ComposeResult:\n        yield LoadingIndicator()\n\n\nif __name__ == \"__main__\":\n    app = LoadingApp()\n    app.run()\n
    "},{"location":"widgets/loading_indicator/#changing-indicator-color","title":"Changing Indicator Color","text":"

    You can set the color of the loading indicator by setting its color style.

    Here's how you would do that with CSS:

    LoadingIndicator {\n    color: red;\n}\n
    "},{"location":"widgets/loading_indicator/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/loading_indicator/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/loading_indicator/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/loading_indicator/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator","title":"textual.widgets.LoadingIndicator class","text":"
    def __init__(\n    self, name=None, id=None, classes=None, disabled=False\n):\n

    Bases: Widget

    Display an animated loading indicator.

    Parameters Parameter Default Description name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/log/","title":"Log","text":"

    Added in version 0.32.0

    A Log widget displays lines of text which may be appended to in realtime.

    Call Log.write_line to write a line at a time, or Log.write_lines to write multiple lines at once. Call Log.clear to clear the Log widget.

    Tip

    See also RichLog which can write more than just text, and supports a number of advanced features.

    • Focusable
    • Container
    "},{"location":"widgets/log/#example","title":"Example","text":"

    The example below shows how to write text to a Log widget:

    Outputlog.py

    LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Log\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass LogApp(App):\n    \"\"\"An app with a simple log.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Log()\n\n    def on_ready(self) -> None:\n        log = self.query_one(Log)\n        log.write_line(\"Hello, World!\")\n        for _ in range(10):\n            log.write_line(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = LogApp()\n    app.run()\n
    "},{"location":"widgets/log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description max_lines int None Maximum number of lines in the log or None for no maximum. auto_scroll bool False Scroll to end of log when new lines are added."},{"location":"widgets/log/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/log/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/log/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/log/#textual.widgets.Log","title":"textual.widgets.Log class","text":"
    def __init__(\n    self,\n    highlight=False,\n    max_lines=None,\n    auto_scroll=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Bases: ScrollView

    A widget to log text.

    Parameters Parameter Default Description highlight bool False

    Enable highlighting.

    max_lines int | None None

    Maximum number of lines to display.

    auto_scroll bool True

    Scroll to end on new lines.

    name str | None None

    The name of the text log.

    id str | None None

    The ID of the text log in the DOM.

    classes str | None None

    The CSS classes of the text log.

    disabled bool False

    Whether the text log is disabled or not.

    "},{"location":"widgets/log/#textual.widgets._log.Log.auto_scroll","title":"auto_scroll instance-attribute class-attribute","text":"
    auto_scroll: var[bool] = auto_scroll\n

    Automatically scroll to new lines.

    "},{"location":"widgets/log/#textual.widgets._log.Log.highlight","title":"highlight instance-attribute","text":"
    highlight = highlight\n

    Enable highlighting.

    "},{"location":"widgets/log/#textual.widgets._log.Log.highlighter","title":"highlighter instance-attribute","text":"
    highlighter = ReprHighlighter()\n

    The Rich Highlighter object to use, if highlight=True

    "},{"location":"widgets/log/#textual.widgets._log.Log.line_count","title":"line_count property","text":"
    line_count: int\n

    Number of lines of content.

    "},{"location":"widgets/log/#textual.widgets._log.Log.lines","title":"lines property","text":"
    lines: Sequence[str]\n

    The raw lines in the Log.

    Note that this attribute is read only. Changing the lines will not update the Log's contents.

    "},{"location":"widgets/log/#textual.widgets._log.Log.max_lines","title":"max_lines instance-attribute class-attribute","text":"
    max_lines: var[int | None] = max_lines\n

    Maximum number of lines to show

    "},{"location":"widgets/log/#textual.widgets._log.Log.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the Log.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.notify_style_update","title":"notify_style_update method","text":"
    def notify_style_update(self):\n

    Called by Textual when styles update.

    "},{"location":"widgets/log/#textual.widgets._log.Log.refresh_lines","title":"refresh_lines method","text":"
    def refresh_lines(self, y_start, line_count=1):\n

    Refresh one or more lines.

    Parameters Parameter Default Description y_start int required

    First line to refresh.

    line_count int 1

    Total number of lines to refresh.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write","title":"write method","text":"
    def write(self, data, scroll_end=None):\n

    Write to the log.

    Parameters Parameter Default Description data str required

    Data to write.

    scroll_end bool | None None

    Scroll to the end after writing, or None to use self.auto_scroll.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write_line","title":"write_line method","text":"
    def write_line(self, line):\n

    Write content on a new line.

    Parameters Parameter Default Description line str required

    String to write to the log.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write_lines","title":"write_lines method","text":"
    def write_lines(self, lines, scroll_end=None):\n

    Write an iterable of lines.

    Parameters Parameter Default Description lines Iterable[str] required

    An iterable of strings to write.

    scroll_end bool | None None

    Scroll to the end after writing, or None to use self.auto_scroll.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/markdown/","title":"Markdown","text":"

    Added in version 0.11.0

    A widget to display a Markdown document.

    • Focusable
    • Container

    Tip

    See MarkdownViewer for a widget that adds additional features such as a Table of Contents.

    "},{"location":"widgets/markdown/#example","title":"Example","text":"

    The following example displays Markdown from a string.

    Outputmarkdown.py

    MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Markdown\n\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Document\n\nThis is an example of Textual's `Markdown` widget.\n\n## Features\n\nMarkdown syntax and extensions are supported.\n\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\"\"\"\n\n\nclass MarkdownExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Markdown(EXAMPLE_MARKDOWN)\n\n\nif __name__ == \"__main__\":\n    app = MarkdownExampleApp()\n    app.run()\n
    "},{"location":"widgets/markdown/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/markdown/#messages","title":"Messages","text":"
    • Markdown.TableOfContentsUpdated
    • Markdown.TableOfContentsSelected
    • Markdown.LinkClicked
    "},{"location":"widgets/markdown/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/markdown/#component-classes","title":"Component Classes","text":"

    The markdown widget provides the following component classes:

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#see-also","title":"See Also","text":"
    • MarkdownViewer code reference
    "},{"location":"widgets/markdown/#textual.widgets.Markdown","title":"textual.widgets.Markdown class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: Widget

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown or None to leave blank for now.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES instance-attribute class-attribute","text":"
    COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
    def __init__(self, markdown, href):\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget containing the link clicked.

    This is an alias for LinkClicked.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
    href: str = href\n

    The link that was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
    def __init__(self, markdown, block_id):\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id: str = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget where the selected item is.

    This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
    def __init__(self, markdown, table_of_contents):\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget associated with the table of contents.

    This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents: TableOfContentsType = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
    def goto_anchor(self, anchor):\n

    Try and find the given anchor in the current document.

    Parameters Parameter Default Description anchor str required

    The anchor to try and find.

    Note

    The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

    Note that the slugging method used is similar to that found on GitHub.

    Returns Type Description bool

    True when the anchor was found in the current document, False otherwise.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
    def load(self, path):\n

    Load a new Markdown document.

    Parameters Parameter Default Description path Path required

    Path to the document.

    Raises Type Description OSError

    If there was some form of error loading the document.

    Note

    The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    def sanitize_location(location):\n

    Given a location, break out the path and any anchor.

    Parameters Parameter Default Description location str required

    The location to sanitize.

    Returns Type Description Path

    A tuple of the path to the location cleaned of any anchor, plus

    str

    the anchor (or an empty string if none was found).

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
    def unhandled_token(self, token):\n

    Process an unhandled token.

    Parameters Parameter Default Description token Token required

    The MarkdownIt token to handle.

    Returns Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
    def update(self, markdown):\n

    Update the document with new Markdown.

    Parameters Parameter Default Description markdown str required

    A string containing Markdown.

    Returns Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown_viewer/","title":"MarkdownViewer","text":"

    Added in version 0.11.0

    A Widget to display Markdown content with an optional Table of Contents.

    • Focusable
    • Container

    Note

    This Widget adds browser-like functionality on top of the Markdown widget.

    "},{"location":"widgets/markdown_viewer/#example","title":"Example","text":"

    The following example displays Markdown from a string and a Table of Contents.

    Outputmarkdown.py

    MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

    from textual.app import App, ComposeResult\nfrom textual.widgets import MarkdownViewer\n\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Viewer\n\nThis is an example of Textual's `MarkdownViewer` widget.\n\n\n## Features\n\nMarkdown syntax and extensions are supported.\n\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\n## Tables\n\nTables are displayed in a DataTable widget.\n\n| Name            | Type   | Default | Description                        |\n| --------------- | ------ | ------- | ---------------------------------- |\n| `show_header`   | `bool` | `True`  | Show the table header              |\n| `fixed_rows`    | `int`  | `0`     | Number of fixed rows               |\n| `fixed_columns` | `int`  | `0`     | Number of fixed columns            |\n| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |\n| `header_height` | `int`  | `1`     | Height of header row               |\n| `show_cursor`   | `bool` | `True`  | Show a cell cursor                 |\n\n\n## Code Blocks\n\nCode blocks are syntax highlighted, with guidelines.\n\n```python\nclass ListViewExample(App):\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n```\n\"\"\"\n\n\nclass MarkdownExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield MarkdownViewer(EXAMPLE_MARKDOWN, show_table_of_contents=True)\n\n\nif __name__ == \"__main__\":\n    app = MarkdownExampleApp()\n    app.run()\n
    "},{"location":"widgets/markdown_viewer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_table_of_contents bool True Wether a Table of Contents should be displayed with the Markdown."},{"location":"widgets/markdown_viewer/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/markdown_viewer/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/markdown_viewer/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/markdown_viewer/#see-also","title":"See Also","text":"
    • Markdown code reference
    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer","title":"textual.widgets.MarkdownViewer class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    show_table_of_contents=True,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: VerticalScroll

    A Markdown viewer widget.

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown, or None to leave blank.

    show_table_of_contents bool True

    Show a table of contents in a sidebar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.document","title":"document property","text":"
    document: Markdown\n

    The Markdown document widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
    table_of_contents: MarkdownTableOfContents\n

    The table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.back","title":"back async","text":"
    def back(self):\n

    Go back one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.forward","title":"forward async","text":"
    def forward(self):\n

    Go forward one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.go","title":"go async","text":"
    def go(self, location):\n

    Navigate to a new document path.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown","title":"textual.widgets.markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.TableOfContentsType","title":"TableOfContentsType module-attribute","text":"
    TableOfContentsType: TypeAlias = (\n    \"list[tuple[int, str, str | None]]\"\n)\n

    Information about the table of contents of a markdown document.

    The triples encode the level, the label, and the optional block id of each heading.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown","title":"Markdown class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: Widget

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown or None to leave blank for now.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES instance-attribute class-attribute","text":"
    COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
    def __init__(self, markdown, href):\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget containing the link clicked.

    This is an alias for LinkClicked.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
    href: str = href\n

    The link that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
    def __init__(self, markdown, block_id):\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id: str = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget where the selected item is.

    This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
    def __init__(self, markdown, table_of_contents):\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget associated with the table of contents.

    This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents: TableOfContentsType = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
    def goto_anchor(self, anchor):\n

    Try and find the given anchor in the current document.

    Parameters Parameter Default Description anchor str required

    The anchor to try and find.

    Note

    The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

    Note that the slugging method used is similar to that found on GitHub.

    Returns Type Description bool

    True when the anchor was found in the current document, False otherwise.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
    def load(self, path):\n

    Load a new Markdown document.

    Parameters Parameter Default Description path Path required

    Path to the document.

    Raises Type Description OSError

    If there was some form of error loading the document.

    Note

    The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    def sanitize_location(location):\n

    Given a location, break out the path and any anchor.

    Parameters Parameter Default Description location str required

    The location to sanitize.

    Returns Type Description Path

    A tuple of the path to the location cleaned of any anchor, plus

    str

    the anchor (or an empty string if none was found).

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
    def unhandled_token(self, token):\n

    Process an unhandled token.

    Parameters Parameter Default Description token Token required

    The MarkdownIt token to handle.

    Returns Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
    def update(self, markdown):\n

    Update the document with new Markdown.

    Parameters Parameter Default Description markdown str required

    A string containing Markdown.

    Returns Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock","title":"MarkdownBlock class","text":"
    def __init__(self, markdown, *args, **kwargs):\n

    Bases: Static

    The base class for a Markdown Element.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownBlock.action_link","title":"action_link async","text":"
    def action_link(self, href):\n

    Called on link click.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents","title":"MarkdownTableOfContents class","text":"
    def __init__(\n    self,\n    markdown,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Bases: Widget

    Displays a table of contents for a markdown document.

    Parameters Parameter Default Description markdown Markdown required

    The Markdown document associated with this table of contents.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown document associated with this table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.table_of_contents","title":"table_of_contents instance-attribute class-attribute","text":"
    table_of_contents = reactive[Optional[TableOfContentsType]](\n    None, init=False\n)\n

    Underlying data to populate the table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.rebuild_table_of_contents","title":"rebuild_table_of_contents method","text":"
    def rebuild_table_of_contents(self, table_of_contents):\n

    Rebuilds the tree representation of the table of contents data.

    Parameters Parameter Default Description table_of_contents TableOfContentsType required

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.watch_table_of_contents","title":"watch_table_of_contents method","text":"
    def watch_table_of_contents(self, table_of_contents):\n

    Triggered when the table of contents changes.

    "},{"location":"widgets/option_list/","title":"OptionList","text":"

    Added in version 0.17.0

    A widget for showing a vertical list of Rich renderable options.

    • Focusable
    • Container
    "},{"location":"widgets/option_list/#examples","title":"Examples","text":""},{"location":"widgets/option_list/#options-as-simple-strings","title":"Options as simple strings","text":"

    An OptionList can be constructed with a simple collection of string options:

    Outputoption_list_strings.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(\n            \"Aerilon\",\n            \"Aquaria\",\n            \"Canceron\",\n            \"Caprica\",\n            \"Gemenon\",\n            \"Leonis\",\n            \"Libran\",\n            \"Picon\",\n            \"Sagittaron\",\n            \"Scorpia\",\n            \"Tauron\",\n            \"Virgon\",\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#options-as-option-instances","title":"Options as Option instances","text":"

    For finer control over the options, the Option class can be used; this allows for setting IDs, setting initial disabled state, etc. The Separator class can be used to add separator lines between options.

    Outputoption_list_options.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nfrom textual.widgets.option_list import Option, Separator\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(\n            Option(\"Aerilon\", id=\"aer\"),\n            Option(\"Aquaria\", id=\"aqu\"),\n            Separator(),\n            Option(\"Canceron\", id=\"can\"),\n            Option(\"Caprica\", id=\"cap\", disabled=True),\n            Separator(),\n            Option(\"Gemenon\", id=\"gem\"),\n            Separator(),\n            Option(\"Leonis\", id=\"leo\"),\n            Option(\"Libran\", id=\"lib\"),\n            Separator(),\n            Option(\"Picon\", id=\"pic\"),\n            Separator(),\n            Option(\"Sagittaron\", id=\"sag\"),\n            Option(\"Scorpia\", id=\"sco\"),\n            Separator(),\n            Option(\"Tauron\", id=\"tau\"),\n            Separator(),\n            Option(\"Virgon\", id=\"vir\"),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#options-as-rich-renderables","title":"Options as Rich renderables","text":"

    Because the prompts for the options can be Rich renderables, this means they can be any height you wish. As an example, here is an option list comprised of Rich tables:

    Outputoption_list_tables.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u2583\u2583\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from __future__ import annotations\n\nfrom rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\n\nCOLONIES: tuple[tuple[str, str, str, str], ...] = (\n    (\"Aerilon\", \"Demeter\", \"1.2 Billion\", \"Gaoth\"),\n    (\"Aquaria\", \"Hermes\", \"75,000\", \"None\"),\n    (\"Canceron\", \"Hephaestus\", \"6.7 Billion\", \"Hades\"),\n    (\"Caprica\", \"Apollo\", \"4.9 Billion\", \"Caprica City\"),\n    (\"Gemenon\", \"Hera\", \"2.8 Billion\", \"Oranu\"),\n    (\"Leonis\", \"Artemis\", \"2.6 Billion\", \"Luminere\"),\n    (\"Libran\", \"Athena\", \"2.1 Billion\", \"None\"),\n    (\"Picon\", \"Poseidon\", \"1.4 Billion\", \"Queenstown\"),\n    (\"Sagittaron\", \"Zeus\", \"1.7 Billion\", \"Tawa\"),\n    (\"Scorpia\", \"Dionysus\", \"450 Million\", \"Celeste\"),\n    (\"Tauron\", \"Ares\", \"2.5 Billion\", \"Hypatia\"),\n    (\"Virgon\", \"Hestia\", \"4.3 Billion\", \"Boskirk\"),\n)\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    @staticmethod\n    def colony(name: str, god: str, population: str, capital: str) -> Table:\n        table = Table(title=f\"Data for {name}\", expand=True)\n        table.add_column(\"Patron God\")\n        table.add_column(\"Population\")\n        table.add_column(\"Capital City\")\n        table.add_row(god, population, capital)\n        return table\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(*[self.colony(*row) for row in COLONIES])\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted option. None means nothing is highlighted."},{"location":"widgets/option_list/#messages","title":"Messages","text":"
    • OptionList.OptionHighlighted
    • OptionList.OptionSelected

    Both of the messages above inherit from the common base OptionList.OptionMessage, so refer to its documentation to see what attributes are available.

    "},{"location":"widgets/option_list/#bindings","title":"Bindings","text":"

    The option list widget defines the following bindings:

    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#component-classes","title":"Component Classes","text":"

    The option list provides the following component classes:

    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.OptionList","title":"textual.widgets.OptionList class","text":"
    def __init__(\n    self,\n    *content,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    wrap=True\n):\n

    Bases: ScrollView

    A vertical option list with bounce-bar highlighting.

    Parameters Parameter Default Description *content NewOptionListContent ()

    The content for the option list.

    name str | None None

    The name of the option list.

    id str | None None

    The ID of the option list in the DOM.

    classes str | None None

    The CSS classes of the option list.

    disabled bool False

    Whether the option list is disabled or not.

    wrap bool True

    Should prompts be auto-wrapped?

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"down\", \"cursor_down\", \"Down\", show=False),\n    Binding(\"end\", \"last\", \"Last\", show=False),\n    Binding(\"enter\", \"select\", \"Select\", show=False),\n    Binding(\"home\", \"first\", \"First\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Up\", show=False),\n]\n
    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"option-list--option\",\n    \"option-list--option-disabled\",\n    \"option-list--option-highlighted\",\n    \"option-list--option-highlighted-disabled\",\n    \"option-list--option-hover\",\n    \"option-list--option-hover-disabled\",\n    \"option-list--option-hover-highlighted\",\n    \"option-list--option-hover-highlighted-disabled\",\n    \"option-list--separator\",\n}\n
    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.highlighted","title":"highlighted instance-attribute class-attribute","text":"
    highlighted: reactive[int | None] = None\n

    The index of the currently-highlighted option, or None if no option is highlighted.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.option_count","title":"option_count property","text":"
    option_count: int\n

    The count of options.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionHighlighted","title":"OptionHighlighted class","text":"

    Bases: OptionMessage

    Message sent when an option is highlighted.

    Can be handled using on_option_list_option_highlighted in a subclass of OptionList or in a parent node in the DOM.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage","title":"OptionMessage class","text":"
    def __init__(self, option_list, index):\n

    Bases: Message

    Base class for all option messages.

    Parameters Parameter Default Description option_list OptionList required

    The option list that owns the option.

    index int required

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.control","title":"control property","text":"
    control: OptionList\n

    The option list that sent the message.

    This is an alias for OptionMessage.option_list and is used by the on decorator.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
    option: Option = option_list.get_option_at_index(index)\n

    The highlighted option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
    option_id: str | None = self.option.id\n

    The ID of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
    option_index: int = index\n

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
    option_list: OptionList = option_list\n

    The option list that sent the message.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionSelected","title":"OptionSelected class","text":"

    Bases: OptionMessage

    Message sent when an option is selected.

    Can be handled using on_option_list_option_selected in a subclass of OptionList or in a parent node in the DOM.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Move the highlight down by one option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Move the highlight up by one option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_first","title":"action_first method","text":"
    def action_first(self):\n

    Move the highlight to the first option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_last","title":"action_last method","text":"
    def action_last(self):\n

    Move the highlight to the last option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the highlight down one page.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the highlight up one page.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_select","title":"action_select method","text":"
    def action_select(self):\n

    Select the currently-highlighted option.

    If no option is selected, then nothing happens. If an option is selected, a OptionList.OptionSelected message will be posted.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_option","title":"add_option method","text":"
    def add_option(self, item=None):\n

    Add a new option to the end of the option list.

    Parameters Parameter Default Description item NewOptionListContent None

    The new item to add.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_options","title":"add_options method","text":"
    def add_options(self, items):\n

    Add new options to the end of the option list.

    Parameters Parameter Default Description items Iterable[NewOptionListContent] required

    The new items to add.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    Note

    All options are checked for duplicate IDs before any option is added. A duplicate ID will cause none of the passed items to be added to the option list.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.clear_options","title":"clear_options method","text":"
    def clear_options(self):\n

    Clear the content of the option list.

    Returns Type Description Self

    The OptionList instance.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option","title":"disable_option method","text":"
    def disable_option(self, option_id):\n

    Disable the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to disable.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option_at_index","title":"disable_option_at_index method","text":"
    def disable_option_at_index(self, index):\n

    Disable the option at the given index.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option","title":"enable_option method","text":"
    def enable_option(self, option_id):\n

    Enable the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to enable.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option_at_index","title":"enable_option_at_index method","text":"
    def enable_option_at_index(self, index):\n

    Enable the option at the given index.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option","title":"get_option method","text":"
    def get_option(self, option_id):\n

    Get the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to get.

    Returns Type Description Option

    The option with the ID.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_at_index","title":"get_option_at_index method","text":"
    def get_option_at_index(self, index):\n

    Get the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to get.

    Returns Type Description Option

    The option at that index.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_index","title":"get_option_index method","text":"
    def get_option_index(self, option_id):\n

    Get the index of the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to get the index of.

    Returns Type Description int

    The index of the item with the given ID.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option","title":"remove_option method","text":"
    def remove_option(self, option_id):\n

    Remove the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to remove.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option_at_index","title":"remove_option_at_index method","text":"
    def remove_option_at_index(self, index):\n

    Remove the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to remove.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt","title":"replace_option_prompt method","text":"
    def replace_option_prompt(self, option_id, prompt):\n

    Replace the prompt of the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to replace the prompt of.

    prompt RenderableType required

    The new prompt for the option.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index method","text":"
    def replace_option_prompt_at_index(self, index, prompt):\n

    Replace the prompt of the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to replace the prompt of.

    prompt RenderableType required

    The new prompt for the option.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.scroll_to_highlight","title":"scroll_to_highlight method","text":"
    def scroll_to_highlight(self, top=False):\n

    Ensure that the highlighted option is in view.

    Parameters Parameter Default Description top bool False

    Scroll highlight to top of the list.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.validate_highlighted","title":"validate_highlighted method","text":"
    def validate_highlighted(self, highlighted):\n

    Validate the highlighted property value on access.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_highlighted","title":"watch_highlighted method","text":"
    def watch_highlighted(self, highlighted):\n

    React to the highlighted option having changed.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_show_vertical_scrollbar","title":"watch_show_vertical_scrollbar method","text":"
    def watch_show_vertical_scrollbar(self):\n

    Handle the vertical scrollbar visibility status changing.

    show_vertical_scrollbar is watched because it has an impact on the available width in which to render the renderables that make up the options in the list. If a vertical scrollbar appears or disappears we need to recalculate all the lines that make up the list.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID class","text":"

    Bases: Exception

    Raised if a duplicate ID is used when adding options to an option list.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option","title":"Option class","text":"
    def __init__(self, prompt, id=None, disabled=False):\n

    Class that holds the details of an individual option.

    Parameters Parameter Default Description prompt RenderableType required

    The prompt for the option.

    id str | None None

    The optional ID for the option.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.id","title":"id property","text":"
    id: str | None\n

    The optional ID for the option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.prompt","title":"prompt property","text":"
    prompt: RenderableType\n

    The prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.set_prompt","title":"set_prompt method","text":"
    def set_prompt(self, prompt):\n

    Set the prompt for the option.

    Parameters Parameter Default Description prompt RenderableType required

    The new prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

    Bases: Exception

    Raised when a request has been made for an option that doesn't exist.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Separator","title":"Separator class","text":"

    Class used to add a separator to an OptionList.

    "},{"location":"widgets/placeholder/","title":"Placeholder","text":"

    Added in version 0.6.0

    A widget that is meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.

    The placeholder widget has variants that display different bits of useful information. Clicking a placeholder will cycle through its variants.

    • Focusable
    • Container
    "},{"location":"widgets/placeholder/#example","title":"Example","text":"

    The example below shows each placeholder variant.

    Outputplaceholder.pyplaceholder.tcss

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass PlaceholderApp(App):\n    CSS_PATH = \"placeholder.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield VerticalScroll(\n            Container(\n                Placeholder(\"This is a custom label for p1.\", id=\"p1\"),\n                Placeholder(\"Placeholder p2 here!\", id=\"p2\"),\n                Placeholder(id=\"p3\"),\n                Placeholder(id=\"p4\"),\n                Placeholder(id=\"p5\"),\n                Placeholder(),\n                Horizontal(\n                    Placeholder(variant=\"size\", id=\"col1\"),\n                    Placeholder(variant=\"text\", id=\"col2\"),\n                    Placeholder(variant=\"size\", id=\"col3\"),\n                    id=\"c1\",\n                ),\n                id=\"bot\",\n            ),\n            Container(\n                Placeholder(variant=\"text\", id=\"left\"),\n                Placeholder(variant=\"size\", id=\"topright\"),\n                Placeholder(variant=\"text\", id=\"botright\"),\n                id=\"top\",\n            ),\n            id=\"content\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = PlaceholderApp()\n    app.run()\n
    Placeholder {\n    height: 100%;\n}\n\n#top {\n    height: 50%;\n    width: 100%;\n    layout: grid;\n    grid-size: 2 2;\n}\n\n#left {\n    row-span: 2;\n}\n\n#bot {\n    height: 50%;\n    width: 100%;\n    layout: grid;\n    grid-size: 8 8;\n}\n\n#c1 {\n    row-span: 4;\n    column-span: 8;\n    height: 100%;\n}\n\n#col1, #col2, #col3 {\n    width: 1fr;\n}\n\n#p1 {\n    row-span: 4;\n    column-span: 4;\n}\n\n#p2 {\n    row-span: 2;\n    column-span: 4;\n}\n\n#p3 {\n    row-span: 2;\n    column-span: 2;\n}\n\n#p4 {\n    row-span: 1;\n    column-span: 2;\n}\n
    "},{"location":"widgets/placeholder/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description variant str \"default\" Styling variant. One of default, size, text."},{"location":"widgets/placeholder/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/placeholder/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/placeholder/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder","title":"textual.widgets.Placeholder class","text":"
    def __init__(\n    self,\n    label=None,\n    variant=\"default\",\n    *,\n    name=None,\n    id=None,\n    classes=None\n):\n

    Bases: Widget

    A simple placeholder widget to use before you build your custom widgets.

    This placeholder has a couple of variants that show different data. Clicking the placeholder cycles through the available variants, but a placeholder can also be initialised in a specific variant.

    The variants available are:

    Variant Placeholder shows default Identifier label or the ID of the placeholder. size Size of the placeholder. text Lorem Ipsum text. Parameters Parameter Default Description label str | None None

    The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

    variant PlaceholderVariant 'default'

    The variant of the placeholder.

    name str | None None

    The name of the placeholder.

    id str | None None

    The ID of the placeholder in the DOM.

    classes str | None None

    A space separated string with the CSS classes of the placeholder, if any.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.variant","title":"variant instance-attribute class-attribute","text":"
    variant: Reactive[\n    PlaceholderVariant\n] = self.validate_variant(variant)\n

    The current variant of the placeholder.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.cycle_variant","title":"cycle_variant method","text":"
    def cycle_variant(self):\n

    Get the next variant in the cycle.

    Returns Type Description Self

    The Placeholder instance.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.validate_variant","title":"validate_variant method","text":"
    def validate_variant(self, variant):\n

    Validate the variant to which the placeholder was set.

    "},{"location":"widgets/pretty/","title":"Pretty","text":"

    Display a pretty-formatted object.

    • Focusable
    • Container
    "},{"location":"widgets/pretty/#example","title":"Example","text":"

    The example below shows a pretty-formatted dict, but Pretty can display any Python object.

    Outputpretty.py

    PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

    from textual.app import App, ComposeResult\nfrom textual.widgets import Pretty\n\nDATA = {\n    \"title\": \"Back to the Future\",\n    \"releaseYear\": 1985,\n    \"director\": \"Robert Zemeckis\",\n    \"genre\": \"Adventure, Comedy, Sci-Fi\",\n    \"cast\": [\n        {\"actor\": \"Michael J. Fox\", \"character\": \"Marty McFly\"},\n        {\"actor\": \"Christopher Lloyd\", \"character\": \"Dr. Emmett Brown\"},\n    ],\n}\n\n\nclass PrettyExample(App):\n    def compose(self) -> ComposeResult:\n        yield Pretty(DATA)\n\n\napp = PrettyExample()\n\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/pretty/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/pretty/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/pretty/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/pretty/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/pretty/#textual.widgets.Pretty","title":"textual.widgets.Pretty class","text":"
    def __init__(self, object, *, name=None, id=None, classes=None):\n

    Bases: Widget

    A pretty-printing widget.

    Used to pretty-print any object.

    Parameters Parameter Default Description object Any required

    The object to pretty-print.

    name str | None None

    The name of the pretty widget.

    id str | None None

    The ID of the pretty in the DOM.

    classes str | None None

    The CSS classes of the pretty.

    "},{"location":"widgets/pretty/#textual.widgets._pretty.Pretty.update","title":"update method","text":"
    def update(self, object):\n

    Update the content of the pretty widget.

    Parameters Parameter Default Description object Any required

    The object to pretty-print.

    "},{"location":"widgets/progress_bar/","title":"ProgressBar","text":"

    A widget that displays progress on a time-consuming task.

    • Focusable
    • Container
    "},{"location":"widgets/progress_bar/#examples","title":"Examples","text":""},{"location":"widgets/progress_bar/#progress-bar-in-isolation","title":"Progress Bar in Isolation","text":"

    The example below shows a progress bar in isolation. It shows the progress bar in:

    • its indeterminate state, when the total progress hasn't been set yet;
    • the middle of the progress; and
    • the completed state.
    Indeterminate state39% doneCompletedprogress_bar_isolated.py

    IndeterminateProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

    IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\n\n\nclass IndeterminateProgressBar(App[None]):\n    BINDINGS = [(\"s\", \"start\", \"Start\")]\n\n    progress_timer: Timer\n    \"\"\"Timer to simulate progress happening.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            with Middle():\n                yield ProgressBar()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Set up a timer to simulate progess happening.\"\"\"\n        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\n\n    def make_progress(self) -> None:\n        \"\"\"Called automatically to advance the progress bar.\"\"\"\n        self.query_one(ProgressBar).advance(1)\n\n    def action_start(self) -> None:\n        \"\"\"Start the progress tracking.\"\"\"\n        self.query_one(ProgressBar).update(total=100)\n        self.progress_timer.resume()\n\n\nif __name__ == \"__main__\":\n    IndeterminateProgressBar().run()\n
    "},{"location":"widgets/progress_bar/#complete-app-example","title":"Complete App Example","text":"

    The example below shows a simple app with a progress bar that is keeping track of a fictitious funding level for an organisation.

    OutputOutput (partial funding)Output (full funding)progress_bar.pyprogress_bar.tcss

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u25010% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250135% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received!

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received! Donation\u00a0for\u00a0$65\u00a0received!

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, VerticalScroll\nfrom textual.widgets import Button, Header, Input, Label, ProgressBar\n\n\nclass FundingProgressApp(App[None]):\n    CSS_PATH = \"progress_bar.tcss\"\n\n    TITLE = \"Funding tracking\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Center():\n            yield Label(\"Funding: \")\n            yield ProgressBar(total=100, show_eta=False)  # (1)!\n        with Center():\n            yield Input(placeholder=\"$$$\")\n            yield Button(\"Donate\")\n\n        yield VerticalScroll(id=\"history\")\n\n    def on_button_pressed(self) -> None:\n        self.add_donation()\n\n    def on_input_submitted(self) -> None:\n        self.add_donation()\n\n    def add_donation(self) -> None:\n        text_value = self.query_one(Input).value\n        try:\n            value = int(text_value)\n        except ValueError:\n            return\n        self.query_one(ProgressBar).advance(value)\n        self.query_one(VerticalScroll).mount(Label(f\"Donation for ${value} received!\"))\n        self.query_one(Input).value = \"\"\n\n\nif __name__ == \"__main__\":\n    FundingProgressApp().run()\n
    1. We create a progress bar with a total of 100 steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task.
    Container {\n    overflow: hidden hidden;\n    height: auto;\n}\n\nCenter {\n    margin-top: 1;\n    margin-bottom: 1;\n    layout: horizontal;\n}\n\nProgressBar {\n    padding-left: 3;\n}\n\nInput {\n    width: 16;\n}\n\nVerticalScroll {\n    height: auto;\n}\n
    "},{"location":"widgets/progress_bar/#custom-styling","title":"Custom Styling","text":"

    This shows a progress bar with custom styling. Refer to the section below for more information.

    Indeterminate state39% doneCompletedprogress_bar_styled.pyprogress_bar_styled.tcss

    StyledProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

    StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\n\n\nclass StyledProgressBar(App[None]):\n    BINDINGS = [(\"s\", \"start\", \"Start\")]\n    CSS_PATH = \"progress_bar_styled.tcss\"\n\n    progress_timer: Timer\n    \"\"\"Timer to simulate progress happening.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            with Middle():\n                yield ProgressBar()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Set up a timer to simulate progess happening.\"\"\"\n        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\n\n    def make_progress(self) -> None:\n        \"\"\"Called automatically to advance the progress bar.\"\"\"\n        self.query_one(ProgressBar).advance(1)\n\n    def action_start(self) -> None:\n        \"\"\"Start the progress tracking.\"\"\"\n        self.query_one(ProgressBar).update(total=100)\n        self.progress_timer.resume()\n\n\nif __name__ == \"__main__\":\n    StyledProgressBar().run()\n
    Bar > .bar--indeterminate {\n    color: $primary;\n    background: $secondary;\n}\n\nBar > .bar--bar {\n    color: $primary;\n    background: $primary 30%;\n}\n\nBar > .bar--complete {\n    color: $error;\n}\n\nPercentageStatus {\n    text-style: reverse;\n    color: $secondary;\n}\n\nETAStatus {\n    text-style: underline;\n}\n
    "},{"location":"widgets/progress_bar/#styling-the-progress-bar","title":"Styling the Progress Bar","text":"

    The progress bar is composed of three sub-widgets that can be styled independently:

    Widget name ID Description Bar #bar The bar that visually represents the progress made. PercentageStatus #percentage Label that shows the percentage of completion. ETAStatus #eta Label that shows the estimated time to completion."},{"location":"widgets/progress_bar/#bar-component-classes","title":"Bar Component Classes","text":"

    The bar sub-widget provides the component classes that follow.

    These component classes let you modify the foreground and background color of the bar in its different states.

    Class Description bar--bar Style of the bar (may be used to change the color). bar--complete Style of the bar when it's complete. bar--indeterminate Style of the bar when it's in an indeterminate state."},{"location":"widgets/progress_bar/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description percentage float | None The read-only percentage of progress that has been made. This is None if the total hasn't been set. progress float 0 The number of steps of progress already made. total float | None The total number of steps that we are keeping track of."},{"location":"widgets/progress_bar/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/progress_bar/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/progress_bar/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar","title":"textual.widgets.ProgressBar class","text":"
    def __init__(\n    self,\n    total=None,\n    *,\n    show_bar=True,\n    show_percentage=True,\n    show_eta=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A progress bar widget.

    The progress bar uses \"steps\" as the measurement unit.

    Example
    class MyApp(App):\n    def compose(self):\n        yield ProgressBar(total=100)\n\n    def key_space(self):\n        self.query_one(ProgressBar).advance(5)\n
    Parameters Parameter Default Description total float | None None

    The total number of steps in the progress if known.

    show_bar bool True

    Whether to show the bar portion of the progress bar.

    show_percentage bool True

    Whether to show the percentage status of the bar.

    show_eta bool True

    Whether to show the ETA countdown of the progress bar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.percentage","title":"percentage instance-attribute class-attribute","text":"
    percentage: reactive[float | None] = reactive[\n    Optional[float]\n](None)\n

    The percentage of progress that has been completed.

    The percentage is a value between 0 and 1 and the returned value is only None if the total progress of the bar hasn't been set yet.

    Example
    progress_bar = ProgressBar()\nprint(progress_bar.percentage)  # None\nprogress_bar.update(total=100)\nprogress_bar.advance(50)\nprint(progress_bar.percentage)  # 0.5\n
    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.progress","title":"progress instance-attribute class-attribute","text":"
    progress: reactive[float] = reactive(0.0)\n

    The progress so far, in number of steps.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.total","title":"total instance-attribute class-attribute","text":"
    total: reactive[float | None] = total\n

    The total number of steps associated with this progress bar, when known.

    The value None will render an indeterminate progress bar.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.advance","title":"advance method","text":"
    def advance(self, advance=1):\n

    Advance the progress of the progress bar by the given amount.

    Example
    progress_bar.advance(10)  # Advance 10 steps.\n
    Parameters Parameter Default Description advance float 1

    Number of steps to advance progress by.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.compute_percentage","title":"compute_percentage method","text":"
    def compute_percentage(self):\n

    Keep the percentage of progress updated automatically.

    This will report a percentage of 1 if the total is zero.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.update","title":"update method","text":"
    def update(\n    self, *, total=UNUSED, progress=UNUSED, advance=UNUSED\n):\n

    Update the progress bar with the given options.

    Example
    progress_bar.update(\n    total=200,  # Set new total to 200 steps.\n    progress=50,  # Set the progress to 50 (out of 200).\n)\n
    Parameters Parameter Default Description total None | float | UnusedParameter UNUSED

    New total number of steps.

    progress float | UnusedParameter UNUSED

    Set the progress to the given number of steps.

    advance float | UnusedParameter UNUSED

    Advance the progress by this number of steps.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_progress","title":"validate_progress method","text":"
    def validate_progress(self, progress):\n

    Clamp the progress between 0 and the maximum total.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_total","title":"validate_total method","text":"
    def validate_total(self, total):\n

    Ensure the total is not negative.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.watch_total","title":"watch_total method","text":"
    def watch_total(self, total):\n

    Re-validate progress.

    "},{"location":"widgets/radiobutton/","title":"RadioButton","text":"

    Added in version 0.13.0

    A simple radio button which stores a boolean value.

    • Focusable
    • Container

    A radio button is best used with others inside a RadioSet.

    "},{"location":"widgets/radiobutton/#example","title":"Example","text":"

    The example below shows radio buttons, used within a RadioSet.

    Outputradio_button.pyradio_button.tcss

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import RadioButton, RadioSet\n\n\nclass RadioChoicesApp(App[None]):\n    CSS_PATH = \"radio_button.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with RadioSet():\n            yield RadioButton(\"Battlestar Galactica\")\n            yield RadioButton(\"Dune 1984\")\n            yield RadioButton(\"Dune 2021\", id=\"focus_me\")\n            yield RadioButton(\"Serenity\", value=True)\n            yield RadioButton(\"Star Trek: The Motion Picture\")\n            yield RadioButton(\"Star Wars: A New Hope\")\n            yield RadioButton(\"The Last Starfighter\")\n            yield RadioButton(\n                \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n            )\n            yield RadioButton(\"Wing Commander\")\n\n    def on_mount(self) -> None:\n        self.query_one(RadioSet).focus()\n\n\nif __name__ == \"__main__\":\n    RadioChoicesApp().run()\n
    Screen {\n    align: center middle;\n}\n\nRadioSet {\n    width: 50%;\n}\n
    "},{"location":"widgets/radiobutton/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the radio button."},{"location":"widgets/radiobutton/#messages","title":"Messages","text":"
    • RadioButton.Changed
    "},{"location":"widgets/radiobutton/#bindings","title":"Bindings","text":"

    The radio button widget defines the following bindings:

    Key(s) Description enter, space Toggle the value."},{"location":"widgets/radiobutton/#component-classes","title":"Component Classes","text":"

    The checkbox widget inherits the following component classes:

    Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/radiobutton/#see-also","title":"See Also","text":"
    • RadioSet
    "},{"location":"widgets/radiobutton/#textual.widgets.RadioButton","title":"textual.widgets.RadioButton class","text":"

    Bases: ToggleButton

    A radio button widget that represents a boolean value.

    Note

    A RadioButton is best used within a RadioSet.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.BUTTON_INNER","title":"BUTTON_INNER instance-attribute class-attribute","text":"
    BUTTON_INNER = '\u25cf'\n

    The character used for the inside of the button.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed","title":"Changed class","text":"

    Bases: ToggleButton.Changed

    Posted when the value of the radio button changes.

    This message can be handled using an on_radio_button_changed method.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.control","title":"control property","text":"
    control: RadioButton\n

    Alias for Changed.radio_button.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.radio_button","title":"radio_button property","text":"
    radio_button: RadioButton\n

    The radio button that was changed.

    "},{"location":"widgets/radioset/","title":"RadioSet","text":"

    Added in version 0.13.0

    A container widget that groups RadioButtons together.

    • Focusable
    • Container
    "},{"location":"widgets/radioset/#example","title":"Example","text":""},{"location":"widgets/radioset/#simple-example","title":"Simple example","text":"

    The example below shows two radio sets, one built using a collection of radio buttons, the other a collection of simple strings.

    Outputradio_set.pyradio_set.tcss

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import RadioButton, RadioSet\n\n\nclass RadioChoicesApp(App[None]):\n    CSS_PATH = \"radio_set.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            # A RadioSet built up from RadioButtons.\n            with RadioSet(id=\"focus_me\"):\n                yield RadioButton(\"Battlestar Galactica\")\n                yield RadioButton(\"Dune 1984\")\n                yield RadioButton(\"Dune 2021\")\n                yield RadioButton(\"Serenity\", value=True)\n                yield RadioButton(\"Star Trek: The Motion Picture\")\n                yield RadioButton(\"Star Wars: A New Hope\")\n                yield RadioButton(\"The Last Starfighter\")\n                yield RadioButton(\n                    \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n                )\n                yield RadioButton(\"Wing Commander\")\n            # A RadioSet built up from a collection of strings.\n            yield RadioSet(\n                \"Amanda\",\n                \"Connor MacLeod\",\n                \"Duncan MacLeod\",\n                \"Heather MacLeod\",\n                \"Joe Dawson\",\n                \"Kurgan, [bold italic red]The[/]\",\n                \"Methos\",\n                \"Rachel Ellenstein\",\n                \"Ram\u00edrez\",\n            )\n\n    def on_mount(self) -> None:\n        self.query_one(\"#focus_me\").focus()\n\n\nif __name__ == \"__main__\":\n    RadioChoicesApp().run()\n
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    align: center middle;\n    height: auto;\n}\n\nRadioSet {\n    width: 45%;\n}\n
    "},{"location":"widgets/radioset/#reacting-to-changes-in-a-radio-set","title":"Reacting to Changes in a Radio Set","text":"

    Here is an example of using the message to react to changes in a RadioSet:

    Outputradio_set_changed.pyradio_set_changed.tcss

    RadioSetChangedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Pictur\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2587\u2587 Pressed\u00a0button\u00a0label:\u00a0Battlestar\u00a0Galactica

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Label, RadioButton, RadioSet\n\n\nclass RadioSetChangedApp(App[None]):\n    CSS_PATH = \"radio_set_changed.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            with Horizontal():\n                with RadioSet(id=\"focus_me\"):\n                    yield RadioButton(\"Battlestar Galactica\")\n                    yield RadioButton(\"Dune 1984\")\n                    yield RadioButton(\"Dune 2021\")\n                    yield RadioButton(\"Serenity\", value=True)\n                    yield RadioButton(\"Star Trek: The Motion Picture\")\n                    yield RadioButton(\"Star Wars: A New Hope\")\n                    yield RadioButton(\"The Last Starfighter\")\n                    yield RadioButton(\n                        \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n                    )\n                    yield RadioButton(\"Wing Commander\")\n            with Horizontal():\n                yield Label(id=\"pressed\")\n            with Horizontal():\n                yield Label(id=\"index\")\n\n    def on_mount(self) -> None:\n        self.query_one(RadioSet).focus()\n\n    def on_radio_set_changed(self, event: RadioSet.Changed) -> None:\n        self.query_one(\"#pressed\", Label).update(\n            f\"Pressed button label: {event.pressed.label}\"\n        )\n        self.query_one(\"#index\", Label).update(\n            f\"Pressed button index: {event.radio_set.pressed_index}\"\n        )\n\n\nif __name__ == \"__main__\":\n    RadioSetChangedApp().run()\n
    VerticalScroll {\n    align: center middle;\n}\n\nHorizontal {\n    align: center middle;\n    height: auto;\n}\n\nRadioSet {\n    width: 45%;\n}\n
    "},{"location":"widgets/radioset/#messages","title":"Messages","text":"
    • RadioSet.Changed
    "},{"location":"widgets/radioset/#bindings","title":"Bindings","text":"

    The RadioSet widget defines the following bindings:

    Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/radioset/#see-also","title":"See Also","text":"
    • RadioButton
    "},{"location":"widgets/radioset/#textual.widgets.RadioSet","title":"textual.widgets.RadioSet class","text":"
    def __init__(\n    self,\n    *buttons,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Container

    Widget for grouping a collection of radio buttons into a set.

    When a collection of RadioButtons are grouped with this widget, they will be treated as a mutually-exclusive grouping. If one button is turned on, the previously-on button will be turned off.

    Parameters Parameter Default Description buttons str | RadioButton ()

    The labels or RadioButtons to group together.

    name str | None None

    The name of the radio set.

    id str | None None

    The ID of the radio set in the DOM.

    classes str | None None

    The CSS classes of the radio set.

    disabled bool False

    Whether the radio set is disabled or not.

    Note

    When a str label is provided, a RadioButton will be created from it.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"down,right\", \"next_button\", \"\", show=False),\n    Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\n    Binding(\"up,left\", \"previous_button\", \"\", show=False),\n]\n
    Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_button","title":"pressed_button property","text":"
    pressed_button: RadioButton | None\n

    The currently-pressed RadioButton, or None if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_index","title":"pressed_index property","text":"
    pressed_index: int\n

    The index of the currently-pressed RadioButton, or -1 if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed","title":"Changed class","text":"
    def __init__(self, radio_set, pressed):\n

    Bases: Message

    Posted when the pressed button in the set changes.

    This message can be handled using an on_radio_set_changed method.

    Parameters Parameter Default Description pressed RadioButton required

    The radio button that was pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'pressed'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.control","title":"control property","text":"
    control: RadioSet\n

    A reference to the RadioSet that was changed.

    This is an alias for Changed.radio_set and is used by the on decorator.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.index","title":"index instance-attribute","text":"
    index = radio_set.pressed_index\n

    The index of the RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.pressed","title":"pressed instance-attribute","text":"
    pressed = pressed\n

    The RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.radio_set","title":"radio_set instance-attribute","text":"
    radio_set = radio_set\n

    A reference to the RadioSet that was changed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_next_button","title":"action_next_button method","text":"
    def action_next_button(self):\n

    Navigate to the next button in the set.

    Note that this will wrap around to the start if at the end.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_previous_button","title":"action_previous_button method","text":"
    def action_previous_button(self):\n

    Navigate to the previous button in the set.

    Note that this will wrap around to the end if at the start.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_toggle","title":"action_toggle method","text":"
    def action_toggle(self):\n

    Toggle the state of the currently-selected button.

    "},{"location":"widgets/rich_log/","title":"RichLog","text":"

    A RichLog is a widget which displays scrollable content that may be appended to in realtime.

    Call RichLog.write with a string or Rich Renderable to write content to the end of the RichLog. Call RichLog.clear to clear the content.

    Tip

    See also Log which is an alternative to RichLog but specialized for simple text.

    • Focusable
    • Container
    "},{"location":"widgets/rich_log/#example","title":"Example","text":"

    The example below shows an application showing a RichLog with different kinds of data logged.

    Outputrich_log.py

    RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

    import csv\nimport io\n\nfrom rich.syntax import Syntax\nfrom rich.table import Table\n\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\nCSV = \"\"\"lane,swimmer,country,time\n4,Joseph Schooling,Singapore,50.39\n2,Michael Phelps,United States,51.14\n5,Chad le Clos,South Africa,51.14\n6,L\u00e1szl\u00f3 Cseh,Hungary,51.14\n3,Li Zhuhao,China,51.26\n8,Mehdy Metella,France,51.58\n7,Tom Shields,United States,51.73\n1,Aleksandr Sadovnikov,Russia,51.84\"\"\"\n\n\nCODE = '''\\\ndef loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:\n    \"\"\"Iterate and generate a tuple with a flag for first and last value.\"\"\"\n    iter_values = iter(values)\n    try:\n        previous_value = next(iter_values)\n    except StopIteration:\n        return\n    first = True\n    for value in iter_values:\n        yield first, False, previous_value\n        first = False\n        previous_value = value\n    yield first, True, previous_value\\\n'''\n\n\nclass RichLogApp(App):\n    def compose(self) -> ComposeResult:\n        yield RichLog(highlight=True, markup=True)\n\n    def on_ready(self) -> None:\n        \"\"\"Called  when the DOM is ready.\"\"\"\n        text_log = self.query_one(RichLog)\n\n        text_log.write(Syntax(CODE, \"python\", indent_guides=True))\n\n        rows = iter(csv.reader(io.StringIO(CSV)))\n        table = Table(*next(rows))\n        for row in rows:\n            table.add_row(*row)\n\n        text_log.write(table)\n        text_log.write(\"[bold magenta]Write text or any Rich renderable!\")\n\n    def on_key(self, event: events.Key) -> None:\n        \"\"\"Write Key events to log.\"\"\"\n        text_log = self.query_one(RichLog)\n        text_log.write(event)\n\n\nif __name__ == \"__main__\":\n    app = RichLogApp()\n    app.run()\n
    "},{"location":"widgets/rich_log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight bool False Automatically highlight content. markup bool False Apply Rich console markup. max_lines int None Maximum number of lines in the log or None for no maximum. min_width int 78 Minimum width of renderables. wrap bool False Enable word wrapping."},{"location":"widgets/rich_log/#messages","title":"Messages","text":"

    This widget sends no messages.

    "},{"location":"widgets/rich_log/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/rich_log/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog","title":"textual.widgets.RichLog class","text":"
    def __init__(\n    self,\n    *,\n    max_lines=None,\n    min_width=78,\n    wrap=False,\n    highlight=False,\n    markup=False,\n    auto_scroll=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView

    A widget for logging text.

    Parameters Parameter Default Description max_lines int | None None

    Maximum number of lines in the log or None for no maximum.

    min_width int 78

    Minimum width of renderables.

    wrap bool False

    Enable word wrapping (default is off).

    highlight bool False

    Automatically highlight content.

    markup bool False

    Apply Rich console markup.

    auto_scroll bool True

    Enable automatic scrolling to end.

    name str | None None

    The name of the text log.

    id str | None None

    The ID of the text log in the DOM.

    classes str | None None

    The CSS classes of the text log.

    disabled bool False

    Whether the text log is disabled or not.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.auto_scroll","title":"auto_scroll instance-attribute class-attribute","text":"
    auto_scroll: var[bool] = auto_scroll\n

    Automatically scroll to the end on write.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.highlight","title":"highlight instance-attribute class-attribute","text":"
    highlight: var[bool] = highlight\n

    Automatically highlight content.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.markup","title":"markup instance-attribute class-attribute","text":"
    markup: var[bool] = markup\n

    Apply Rich console markup.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.max_lines","title":"max_lines instance-attribute class-attribute","text":"
    max_lines: var[int | None] = max_lines\n

    Maximum number of lines in the log or None for no maximum.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.min_width","title":"min_width instance-attribute class-attribute","text":"
    min_width: var[int] = min_width\n

    Minimum width of renderables.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.wrap","title":"wrap instance-attribute class-attribute","text":"
    wrap: var[bool] = wrap\n

    Enable word wrapping.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the text log.

    Returns Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.write","title":"write method","text":"
    def write(\n    self,\n    content,\n    width=None,\n    expand=False,\n    shrink=True,\n    scroll_end=None,\n):\n

    Write text or a rich renderable.

    Parameters Parameter Default Description content RenderableType | object required

    Rich renderable (or text).

    width int | None None

    Width to render or None to use optimal width.

    expand bool False

    Enable expand to widget width, or False to use width.

    shrink bool True

    Enable shrinking of content to fit width.

    scroll_end bool | None None

    Enable automatic scroll to end, or None to use self.auto_scroll.

    Returns Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rule/","title":"Rule","text":"

    A rule widget to separate content, similar to a <hr> HTML tag.

    • Focusable
    • Container
    "},{"location":"widgets/rule/#examples","title":"Examples","text":""},{"location":"widgets/rule/#horizontal-rule","title":"Horizontal Rule","text":"

    The default orientation of a rule is horizontal.

    The example below shows horizontal rules with all the available line styles.

    Outputhorizontal_rules.pyhorizontal_rules.tcss

    HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

    from textual.app import App, ComposeResult\nfrom textual.containers import Vertical\nfrom textual.widgets import Label, Rule\n\n\nclass HorizontalRulesApp(App):\n    CSS_PATH = \"horizontal_rules.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Vertical():\n            yield Label(\"solid (default)\")\n            yield Rule()\n            yield Label(\"heavy\")\n            yield Rule(line_style=\"heavy\")\n            yield Label(\"thick\")\n            yield Rule(line_style=\"thick\")\n            yield Label(\"dashed\")\n            yield Rule(line_style=\"dashed\")\n            yield Label(\"double\")\n            yield Rule(line_style=\"double\")\n            yield Label(\"ascii\")\n            yield Rule(line_style=\"ascii\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalRulesApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nVertical {\n    height: auto;\n    width: 80%;\n}\n\nLabel {\n    width: 100%;\n    text-align: center;\n}\n
    "},{"location":"widgets/rule/#vertical-rule","title":"Vertical Rule","text":"

    The example below shows vertical rules with all the available line styles.

    Outputvertical_rules.pyvertical_rules.tcss

    VerticalRulesApp solid\u00a0\u2502heavy\u00a0\u2503thick\u00a0\u2588dashed\u254fdouble\u2551ascii\u00a0| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551|

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Rule\n\n\nclass VerticalRulesApp(App):\n    CSS_PATH = \"vertical_rules.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Label(\"solid\")\n            yield Rule(orientation=\"vertical\")\n            yield Label(\"heavy\")\n            yield Rule(orientation=\"vertical\", line_style=\"heavy\")\n            yield Label(\"thick\")\n            yield Rule(orientation=\"vertical\", line_style=\"thick\")\n            yield Label(\"dashed\")\n            yield Rule(orientation=\"vertical\", line_style=\"dashed\")\n            yield Label(\"double\")\n            yield Rule(orientation=\"vertical\", line_style=\"double\")\n            yield Label(\"ascii\")\n            yield Rule(orientation=\"vertical\", line_style=\"ascii\")\n\n\nif __name__ == \"__main__\":\n    app = VerticalRulesApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    width: auto;\n    height: 80%;\n}\n\nLabel {\n    width: 6;\n    height: 100%;\n    text-align: center;\n}\n
    "},{"location":"widgets/rule/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description orientation RuleOrientation \"horizontal\" The orientation of the rule. line_style LineStyle \"solid\" The line style of the rule."},{"location":"widgets/rule/#messages","title":"Messages","text":"

    This widget sends no messages.

    "},{"location":"widgets/rule/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/rule/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/rule/#textual.widgets.Rule","title":"textual.widgets.Rule class","text":"
    def __init__(\n    self,\n    orientation=\"horizontal\",\n    line_style=\"solid\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A rule widget to separate content, similar to a <hr> HTML tag.

    Parameters Parameter Default Description orientation RuleOrientation 'horizontal'

    The orientation of the rule.

    line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.line_style","title":"line_style instance-attribute class-attribute","text":"
    line_style: Reactive[LineStyle] = line_style\n

    The line style of the rule.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.orientation","title":"orientation instance-attribute class-attribute","text":"
    orientation: Reactive[RuleOrientation] = orientation\n

    The orientation of the rule.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.horizontal","title":"horizontal classmethod","text":"
    def horizontal(\n    cls,\n    line_style=\"solid\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Utility constructor for creating a horizontal rule.

    Parameters Parameter Default Description line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    Returns Type Description Rule

    A rule widget with horizontal orientation.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.vertical","title":"vertical classmethod","text":"
    def vertical(\n    cls,\n    line_style=\"solid\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Utility constructor for creating a vertical rule.

    Parameters Parameter Default Description line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    Returns Type Description Rule

    A rule widget with vertical orientation.

    "},{"location":"widgets/rule/#textual.widgets.rule","title":"textual.widgets.rule","text":""},{"location":"widgets/rule/#textual.widgets.rule.LineStyle","title":"LineStyle module-attribute","text":"
    LineStyle = Literal[\n    \"ascii\",\n    \"blank\",\n    \"dashed\",\n    \"double\",\n    \"heavy\",\n    \"hidden\",\n    \"none\",\n    \"solid\",\n    \"thick\",\n]\n

    The valid line styles of the rule widget.

    "},{"location":"widgets/rule/#textual.widgets.rule.RuleOrientation","title":"RuleOrientation module-attribute","text":"
    RuleOrientation = Literal['horizontal', 'vertical']\n

    The valid orientations of the rule widget.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidLineStyle","title":"InvalidLineStyle class","text":"

    Bases: Exception

    Exception raised for an invalid rule line style.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation class","text":"

    Bases: Exception

    Exception raised for an invalid rule orientation.

    "},{"location":"widgets/select/","title":"Select","text":"

    Added in version 0.24.0

    A Select widget is a compact control to allow the user to select between a number of possible options.

    • Focusable
    • Container

    The options in a select control may be passed in to the constructor or set later with set_options. Options should be given as a sequence of tuples consisting of two values: the first is the string (or Rich Renderable) to display in the control and list of options, the second is the value of option.

    The value of the currently selected option is stored in the value attribute of the widget, and the value attribute of the Changed message.

    "},{"location":"widgets/select/#typing","title":"Typing","text":"

    The Select control is a typing Generic which allows you to set the type of the option values. For instance, if the data type for your values is an integer, you would type the widget as follows:

    options = [(\"First\", 1), (\"Second\", 2)]\nmy_select: Select[int] =  Select(options)\n

    Note

    Typing is entirely optional.

    If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

    "},{"location":"widgets/select/#examples","title":"Examples","text":""},{"location":"widgets/select/#basic-example","title":"Basic Example","text":"

    The following example presents a Select with a number of options.

    OutputOutput (expanded)select_widget.pyselect.tcss

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select((line, line) for line in LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    Screen {\n    align: center top;\n}\n\nSelect {\n    width: 60;\n    margin: 2;\n}\n
    "},{"location":"widgets/select/#example-using-class-method","title":"Example using Class Method","text":"

    The following example presents a Select created using the from_values class method.

    OutputOutput (expanded)select_from_values_widget.pyselect.tcss

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select.from_values(LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    Screen {\n    align: center top;\n}\n\nSelect {\n    width: 60;\n    margin: 2;\n}\n
    "},{"location":"widgets/select/#blank-state","title":"Blank state","text":"

    The widget Select has an option allow_blank for its constructor. If set to True, the widget may be in a state where there is no selection, in which case its value will be the special constant Select.BLANK. The auxiliary methods Select.is_blank and Select.clear provide a convenient way to check if the widget is in this state and to set this state, respectively.

    "},{"location":"widgets/select/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description expanded bool False True to expand the options overlay. value SelectType | _NoSelection Select.BLANK Current value of the Select."},{"location":"widgets/select/#messages","title":"Messages","text":"
    • Select.Changed
    "},{"location":"widgets/select/#bindings","title":"Bindings","text":"

    The Select widget defines the following bindings:

    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/select/#textual.widgets.Select","title":"textual.widgets.Select class","text":"
    def __init__(\n    self,\n    options,\n    *,\n    prompt=\"Select\",\n    allow_blank=True,\n    value=BLANK,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[SelectType], Vertical

    Widget to select from a list of possible options.

    A Select displays the current selection. When activated with Enter the widget displays an overlay with a list of all possible options.

    Parameters Parameter Default Description options Iterable[tuple[RenderableType, SelectType]] required

    Options to select from. If no options are provided then allow_blank must be set to True.

    prompt str 'Select'

    Text to show in the control when no option is selected.

    allow_blank bool True

    Enables or disables the ability to have the widget in a state with no selection made, in which case its value is set to the constant Select.BLANK.

    value SelectType | NoSelection BLANK

    Initial value selected. Should be one of the values in options. If no initial value is set and allow_blank is False, the widget will auto-select the first available option.

    name str | None None

    The name of the select control.

    id str | None None

    The ID of the control in the DOM.

    classes str | None None

    The CSS classes of the control.

    disabled bool False

    Whether the control is disabled or not.

    Raises Type Description EmptySelectError

    If no options are provided and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
    BINDINGS = [('enter,down,space,up', 'show_overlay')]\n
    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets._select.Select.BLANK","title":"BLANK instance-attribute class-attribute","text":"
    BLANK = BLANK\n

    Constant to flag that the widget has no selection.

    "},{"location":"widgets/select/#textual.widgets._select.Select.expanded","title":"expanded instance-attribute class-attribute","text":"
    expanded: var[bool] = var(False, init=False)\n

    True to show the overlay, otherwise False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.prompt","title":"prompt instance-attribute class-attribute","text":"
    prompt: var[str] = prompt\n

    The prompt to show when no value is selected.

    "},{"location":"widgets/select/#textual.widgets._select.Select.value","title":"value instance-attribute class-attribute","text":"
    value: var[SelectType | NoSelection] = var[\n    Union[SelectType, NoSelection]\n](BLANK)\n

    The value of the selection.

    If the widget has no selection, its value will be Select.BLANK. Setting this to an illegal value will raise a InvalidSelectValueError exception.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed","title":"Changed class","text":"
    def __init__(self, select, value):\n

    Bases: Message

    Posted when the select value was changed.

    This message can be handled using a on_select_changed method.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.control","title":"control property","text":"
    control: Select[SelectType]\n

    The Select that sent the message.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.select","title":"select instance-attribute","text":"
    select = select\n

    The select widget.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.value","title":"value instance-attribute","text":"
    value = value\n

    The value of the Select when it changed.

    "},{"location":"widgets/select/#textual.widgets._select.Select.action_show_overlay","title":"action_show_overlay method","text":"
    def action_show_overlay(self):\n

    Show the overlay.

    "},{"location":"widgets/select/#textual.widgets._select.Select.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the selection if allow_blank is True.

    Raises Type Description InvalidSelectValueError

    If allow_blank is set to False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.from_values","title":"from_values classmethod","text":"
    def from_values(\n    cls,\n    values,\n    *,\n    prompt=\"Select\",\n    allow_blank=True,\n    value=BLANK,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Initialize the Select control with values specified by an arbitrary iterable

    The options shown in the control are computed by calling the built-in str on each value.

    Parameters Parameter Default Description values Iterable[SelectType] required

    Values used to generate options to select from.

    prompt str 'Select'

    Text to show in the control when no option is selected.

    allow_blank bool True

    Enables or disables the ability to have the widget in a state with no selection made, in which case its value is set to the constant Select.BLANK.

    value SelectType | NoSelection BLANK

    Initial value selected. Should be one of the values in values. If no initial value is set and allow_blank is False, the widget will auto-select the first available value.

    name str | None None

    The name of the select control.

    id str | None None

    The ID of the control in the DOM.

    classes str | None None

    The CSS classes of the control.

    disabled bool False

    Whether the control is disabled or not.

    Returns Type Description Select[SelectType]

    A new Select widget with the provided values as options.

    "},{"location":"widgets/select/#textual.widgets._select.Select.is_blank","title":"is_blank method","text":"
    def is_blank(self):\n

    Indicates whether this Select is blank or not.

    Returns Type Description bool

    True if the selection is blank, False otherwise.

    "},{"location":"widgets/select/#textual.widgets._select.Select.set_options","title":"set_options method","text":"
    def set_options(self, options):\n

    Set the options for the Select.

    This will reset the selection. The selection will be empty, if allowed, otherwise the first valid option is picked.

    Parameters Parameter Default Description options Iterable[tuple[RenderableType, SelectType]] required

    An iterable of tuples containing the renderable to display for each option and the corresponding internal value.

    Raises Type Description EmptySelectError

    If the options iterable is empty and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.select.EmptySelectError","title":"EmptySelectError class","text":"

    Bases: Exception

    Raised when a Select has no options and allow_blank=False.

    "},{"location":"widgets/select/#textual.widgets.select.InvalidSelectValueError","title":"InvalidSelectValueError class","text":"

    Bases: Exception

    Raised when setting a Select to an unknown option.

    "},{"location":"widgets/selection_list/","title":"SelectionList","text":"

    Added in version 0.27.0

    A widget for showing a vertical list of selectable options.

    • Focusable
    • Container
    "},{"location":"widgets/selection_list/#typing","title":"Typing","text":"

    The SelectionList control is a Generic, which allows you to set the type of the selection values. For instance, if the data type for your values is an integer, you would type the widget as follows:

    selections = [(\"First\", 1), (\"Second\", 2)]\nmy_selection_list: SelectionList[int] =  SelectionList(*selections)\n

    Note

    Typing is entirely optional.

    If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

    "},{"location":"widgets/selection_list/#examples","title":"Examples","text":"

    A selection list is designed to be built up of single-line prompts (which can be Rich renderables) and an associated unique value.

    "},{"location":"widgets/selection_list/#selections-as-tuples","title":"Selections as tuples","text":"

    A selection list can be built with tuples, either of two or three values in length. Each tuple must contain a prompt and a value, and it can also optionally contain a flag for the initial selected state of the option.

    Outputselection_list_tuples.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield SelectionList[int](  # (1)!\n            (\"Falken's Maze\", 0, True),\n            (\"Black Jack\", 1),\n            (\"Gin Rummy\", 2),\n            (\"Hearts\", 3),\n            (\"Bridge\", 4),\n            (\"Checkers\", 5),\n            (\"Chess\", 6, True),\n            (\"Poker\", 7),\n            (\"Fighter Combat\", 8, True),\n        )\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as int, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 80%;\n    height: 80%;\n}\n
    "},{"location":"widgets/selection_list/#selections-as-selection-objects","title":"Selections as Selection objects","text":"

    Alternatively, selections can be passed in as Selections:

    Outputselection_list_selections.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nfrom textual.widgets.selection_list import Selection\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield SelectionList[int](  # (1)!\n            Selection(\"Falken's Maze\", 0, True),\n            Selection(\"Black Jack\", 1),\n            Selection(\"Gin Rummy\", 2),\n            Selection(\"Hearts\", 3),\n            Selection(\"Bridge\", 4),\n            Selection(\"Checkers\", 5),\n            Selection(\"Chess\", 6, True),\n            Selection(\"Poker\", 7),\n            Selection(\"Fighter Combat\", 8, True),\n        )\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as int, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 80%;\n    height: 80%;\n}\n
    "},{"location":"widgets/selection_list/#handling-changes-to-the-selections","title":"Handling changes to the selections","text":"

    Most of the time, when using the SelectionList, you will want to know when the collection of selected items has changed; this is ideally done using the SelectedChanged message. Here is an example of using that message to update a Pretty with the collection of selected values:

    Outputselection_list_selections.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2510\u250c\u2500\u00a0Selected\u00a0games\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502[\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502\u2502'secret_back_door',\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502\u2502'a_nice_game_of_chess',\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502\u2502'fighter_combat'\u2502 \u2502\u2590X\u258cHearts\u2502\u2502]\u2502 \u2502\u2590X\u258cBridge\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.events import Mount\nfrom textual.widgets import Footer, Header, Pretty, SelectionList\nfrom textual.widgets.selection_list import Selection\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list_selected.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Horizontal():\n            yield SelectionList[str](  # (1)!\n                Selection(\"Falken's Maze\", \"secret_back_door\", True),\n                Selection(\"Black Jack\", \"black_jack\"),\n                Selection(\"Gin Rummy\", \"gin_rummy\"),\n                Selection(\"Hearts\", \"hearts\"),\n                Selection(\"Bridge\", \"bridge\"),\n                Selection(\"Checkers\", \"checkers\"),\n                Selection(\"Chess\", \"a_nice_game_of_chess\", True),\n                Selection(\"Poker\", \"poker\"),\n                Selection(\"Fighter Combat\", \"fighter_combat\", True),\n            )\n            yield Pretty([])\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n        self.query_one(Pretty).border_title = \"Selected games\"\n\n    @on(Mount)\n    @on(SelectionList.SelectedChanged)\n    def update_selected_view(self) -> None:\n        self.query_one(Pretty).update(self.query_one(SelectionList).selected)\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as str, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    width: 80%;\n    height: 80%;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 1fr;\n}\n\nPretty {\n    width: 1fr;\n    border: solid $accent;\n}\n
    "},{"location":"widgets/selection_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted selection. None means nothing is highlighted."},{"location":"widgets/selection_list/#messages","title":"Messages","text":"

    The following messages will be posted as the user interacts with the list:

    • SelectionList.SelectionHighlighted
    • SelectionList.SelectionToggled

    The following message will be posted if the content of selected changes, either by user interaction or by API calls:

    • SelectionList.SelectedChanged
    "},{"location":"widgets/selection_list/#bindings","title":"Bindings","text":"

    The selection list widget defines the following bindings:

    Key(s) Description space Toggle the state of the highlighted selection.

    It inherits from OptionList and so also inherits the following bindings:

    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/selection_list/#component-classes","title":"Component Classes","text":"

    The selection list provides the following component classes:

    Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style.

    It inherits from OptionList and so also makes use of the following component classes:

    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/selection_list/#textual.widgets.SelectionList","title":"textual.widgets.SelectionList class","text":"
    def __init__(\n    self,\n    *selections,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[SelectionType], OptionList

    A vertical selection list that allows making multiple selections.

    Parameters Parameter Default Description *selections Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool] ()

    The content for the selection list.

    name str | None None

    The name of the selection list.

    id str | None None

    The ID of the selection list in the DOM.

    classes str | None None

    The CSS classes of the selection list.

    disabled bool False

    Whether the selection list is disabled or not.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
    BINDINGS = [Binding('space', 'select')]\n
    Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"selection-list--button\",\n    \"selection-list--button-selected\",\n    \"selection-list--button-highlighted\",\n    \"selection-list--button-selected-highlighted\",\n}\n
    Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.selected","title":"selected property","text":"
    selected: list[SelectionType]\n

    The selected values.

    This is a list of all of the values associated with selections in the list that are currently in the selected state.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged","title":"SelectedChanged class","text":"

    Bases: Generic[MessageSelectionType], Message

    Message sent when the collection of selected values changes.

    This message is sent when any change to the collection of selected values takes place; either by user interaction or by API calls.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.control","title":"control property","text":"
    control: SelectionList[MessageSelectionType]\n

    An alias for selection_list.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
    selection_list: SelectionList[MessageSelectionType]\n

    The SelectionList that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionHighlighted","title":"SelectionHighlighted class","text":"

    Bases: SelectionMessage

    Message sent when a selection is highlighted.

    Can be handled using on_selection_list_selection_highlighted in a subclass of SelectionList or in a parent node in the DOM.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage","title":"SelectionMessage class","text":"
    def __init__(self, selection_list, index):\n

    Bases: Generic[MessageSelectionType], Message

    Base class for all selection messages.

    Parameters Parameter Default Description selection_list SelectionList required

    The selection list that owns the selection.

    index int required

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.control","title":"control property","text":"
    control: OptionList\n

    The selection list that sent the message.

    This is an alias for SelectionMessage.selection_list and is used by the on decorator.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
    selection: Selection[\n    MessageSelectionType\n] = selection_list.get_option_at_index(index)\n

    The highlighted selection.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
    selection_index: int = index\n

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
    selection_list: SelectionList[\n    MessageSelectionType\n] = selection_list\n

    The selection list that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionToggled","title":"SelectionToggled class","text":"

    Bases: SelectionMessage

    Message sent when a selection is toggled.

    Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

    Note

    This message is only sent if the selection is toggled by user interaction. See SelectedChanged for a message sent when any change (selected or deselected, either by user interaction or by API calls) is made to the selected values.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_option","title":"add_option method","text":"
    def add_option(self, item=None):\n

    Add a new selection option to the end of the list.

    Parameters Parameter Default Description item NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool] None

    The new item to add.

    Returns Type Description Self

    The SelectionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    SelectionError

    If the selection option is of the wrong form.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_options","title":"add_options method","text":"
    def add_options(self, items):\n

    Add new selection options to the end of the list.

    Parameters Parameter Default Description items Iterable[NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]] required

    The new items to add.

    Returns Type Description Self

    The SelectionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    SelectionError

    If one of the selection options is of the wrong form.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.clear_options","title":"clear_options method","text":"
    def clear_options(self):\n

    Clear the content of the selection list.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect","title":"deselect method","text":"
    def deselect(self, selection):\n

    Mark the given selection as not selected.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to mark as not selected.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect_all","title":"deselect_all method","text":"
    def deselect_all(self):\n

    Deselect all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option","title":"get_option method","text":"
    def get_option(self, option_id):\n

    Get the selection option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the selection option to get.

    Returns Type Description Selection[SelectionType]

    The selection option with the ID.

    Raises Type Description OptionDoesNotExist

    If no selection option has the given ID.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option_at_index","title":"get_option_at_index method","text":"
    def get_option_at_index(self, index):\n

    Get the selection option at the given index.

    Parameters Parameter Default Description index int required

    The index of the selection option to get.

    Returns Type Description Selection[SelectionType]

    The selection option at that index.

    Raises Type Description OptionDoesNotExist

    If there is no selection option with the index.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select","title":"select method","text":"
    def select(self, selection):\n

    Mark the given selection as selected.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to mark as selected.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select_all","title":"select_all method","text":"
    def select_all(self):\n

    Select all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle","title":"toggle method","text":"
    def toggle(self, selection):\n

    Toggle the selected state of the given selection.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to toggle.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle_all","title":"toggle_all method","text":"
    def toggle_all(self):\n

    Toggle all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.MessageSelectionType","title":"MessageSelectionType module-attribute","text":"
    MessageSelectionType = TypeVar('MessageSelectionType')\n

    The type for the value of a Selection in a SelectionList message.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionType","title":"SelectionType module-attribute","text":"
    SelectionType = TypeVar('SelectionType')\n

    The type for the value of a Selection in a SelectionList

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection","title":"Selection class","text":"
    def __init__(\n    self,\n    prompt,\n    value,\n    initial_state=False,\n    id=None,\n    disabled=False,\n):\n

    Bases: Generic[SelectionType], Option

    A selection for a SelectionList.

    Parameters Parameter Default Description prompt TextType required

    The prompt for the selection.

    value SelectionType required

    The value for the selection.

    initial_state bool False

    The initial selected state of the selection.

    id str | None None

    The optional ID for the selection.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.initial_state","title":"initial_state property","text":"
    initial_state: bool\n

    The initial selected state for the selection.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.value","title":"value property","text":"
    value: SelectionType\n

    The value for this selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError class","text":"

    Bases: TypeError

    Type of an error raised if a selection is badly-formed.

    "},{"location":"widgets/sparkline/","title":"Sparkline","text":"

    Added in version 0.27.0

    A widget that is used to visually represent numerical data.

    • Focusable
    • Container
    "},{"location":"widgets/sparkline/#examples","title":"Examples","text":""},{"location":"widgets/sparkline/#basic-example","title":"Basic example","text":"

    The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.

    Tip

    The sparkline data is split into equally-sized chunks. Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.

    Outputsparkline_basic.pysparkline_basic.tcss

    SparklineBasicApp \u2582\u2584\u2588

    from textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\ndata = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2]  # (1)!\n\n\nclass SparklineBasicApp(App[None]):\n    CSS_PATH = \"sparkline_basic.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Sparkline(  # (2)!\n            data,  # (3)!\n            summary_function=max,  # (4)!\n        )\n\n\napp = SparklineBasicApp()\nif __name__ == \"__main__\":\n    app.run()\n
    1. We have 12 data points.
    2. This sparkline will have its width set to 3 via CSS.
    3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
    4. Each bar will represent its largest value. The largest value of each chunk is 2, 4, and 8, respectively. That explains why the first bar is half the height of the second and the second bar is half the height of the third.
    Screen {\n    align: center middle;\n}\n\nSparkline {\n    width: 3;  /* (1)! */\n    margin: 2;\n}\n
    1. By setting the width to 3 we get three buckets.
    "},{"location":"widgets/sparkline/#different-summary-functions","title":"Different summary functions","text":"

    The example below shows a sparkline widget with different summary functions. The summary function is what determines the height of each bar.

    Outputsparkline.pysparkline.tcss

    SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    import random\nfrom statistics import mean\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\nrandom.seed(73)\ndata = [random.expovariate(1 / 3) for _ in range(1000)]\n\n\nclass SparklineSummaryFunctionApp(App[None]):\n    CSS_PATH = \"sparkline.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Sparkline(data, summary_function=max)  # (1)!\n        yield Sparkline(data, summary_function=mean)  # (2)!\n        yield Sparkline(data, summary_function=min)  # (3)!\n\n\napp = SparklineSummaryFunctionApp()\nif __name__ == \"__main__\":\n    app.run()\n
    1. Each bar will show the largest value of that bucket.
    2. Each bar will show the mean value of that bucket.
    3. Each bar will show the smaller value of that bucket.
    Sparkline {\n    width: 100%;\n    margin: 2;\n}\n
    "},{"location":"widgets/sparkline/#changing-the-colors","title":"Changing the colors","text":"

    The example below shows how to use component classes to change the colors of the sparkline.

    Outputsparkline_colors.pysparkline_colors.tcss

    SparklineColorsApp \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582

    from math import sin\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\n\nclass SparklineColorsApp(App[None]):\n    CSS_PATH = \"sparkline_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        nums = [abs(sin(x / 3.14)) for x in range(0, 360 * 6, 20)]\n        yield Sparkline(nums, summary_function=max, id=\"fst\")\n        yield Sparkline(nums, summary_function=max, id=\"snd\")\n        yield Sparkline(nums, summary_function=max, id=\"trd\")\n        yield Sparkline(nums, summary_function=max, id=\"frt\")\n        yield Sparkline(nums, summary_function=max, id=\"fft\")\n        yield Sparkline(nums, summary_function=max, id=\"sxt\")\n        yield Sparkline(nums, summary_function=max, id=\"svt\")\n        yield Sparkline(nums, summary_function=max, id=\"egt\")\n        yield Sparkline(nums, summary_function=max, id=\"nnt\")\n        yield Sparkline(nums, summary_function=max, id=\"tnt\")\n\n\napp = SparklineColorsApp()\nif __name__ == \"__main__\":\n    app.run()\n
    Sparkline {\n    width: 100%;\n    margin: 1;\n}\n\n#fst > .sparkline--max-color {\n    color: $success;\n}\n#fst > .sparkline--min-color {\n    color: $warning;\n}\n\n#snd > .sparkline--max-color {\n    color: $warning;\n}\n#snd > .sparkline--min-color {\n    color: $success;\n}\n\n#trd > .sparkline--max-color {\n    color: $error;\n}\n#trd > .sparkline--min-color {\n    color: $warning;\n}\n\n#frt > .sparkline--max-color {\n    color: $warning;\n}\n#frt > .sparkline--min-color {\n    color: $error;\n}\n\n#fft > .sparkline--max-color {\n    color: $accent;\n}\n#fft > .sparkline--min-color {\n    color: $accent 30%;\n}\n\n#sxt > .sparkline--max-color {\n    color: $accent 30%;\n}\n#sxt > .sparkline--min-color {\n    color: $accent;\n}\n\n#svt > .sparkline--max-color {\n    color: $error;\n}\n#svt > .sparkline--min-color {\n    color: $error 30%;\n}\n\n#egt > .sparkline--max-color {\n    color: $error 30%;\n}\n#egt > .sparkline--min-color {\n    color: $error;\n}\n\n#nnt > .sparkline--max-color {\n    color: $success;\n}\n#nnt > .sparkline--min-color {\n    color: $success 30%;\n}\n\n#tnt > .sparkline--max-color {\n    color: $success 30%;\n}\n#tnt > .sparkline--min-color {\n    color: $success;\n}\n
    "},{"location":"widgets/sparkline/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description data Sequence[float] | None None The data represented by the sparkline. summary_function Callable[[Sequence[float]], float] max The function that computes the height of each bar."},{"location":"widgets/sparkline/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/sparkline/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/sparkline/#component-classes","title":"Component Classes","text":"

    The sparkline widget provides the following component classes:

    Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

    Note

    These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

    Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets.Sparkline","title":"textual.widgets.Sparkline class","text":"
    def __init__(\n    self,\n    data=None,\n    *,\n    summary_function=None,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A sparkline widget to display numerical data.

    Parameters Parameter Default Description data Sequence[float] | None None

    The initial data to populate the sparkline with.

    summary_function Callable[[Sequence[float]], float] | None None

    Summarises bar values into a single value used to represent each bar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"sparkline--max-color\",\n    \"sparkline--min-color\",\n}\n

    Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

    Note

    These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

    Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.data","title":"data instance-attribute class-attribute","text":"
    data = data\n

    The data that populates the sparkline.

    "},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.summary_function","title":"summary_function instance-attribute class-attribute","text":"
    summary_function = reactive[\n    Callable[[Sequence[float]], float]\n](_max_factory)\n

    The function that computes the value that represents each bar.

    "},{"location":"widgets/static/","title":"Static","text":"

    A widget which displays static content. Can be used for Rich renderables and can also be the base for other types of widgets.

    • Focusable
    • Container
    "},{"location":"widgets/static/#example","title":"Example","text":"

    The example below shows how you can use a Static widget as a simple text label (but see Label as a way of displaying text).

    Outputstatic.py

    StaticApp Hello,\u00a0world!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass StaticApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, world!\")\n\n\nif __name__ == \"__main__\":\n    app = StaticApp()\n    app.run()\n
    "},{"location":"widgets/static/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/static/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/static/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/static/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/static/#see-also","title":"See Also","text":"
    • Label
    • Pretty
    "},{"location":"widgets/static/#textual.widgets.Static","title":"textual.widgets.Static class","text":"
    def __init__(\n    self,\n    renderable=\"\",\n    *,\n    expand=False,\n    shrink=False,\n    markup=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A widget to display simple static content, or use as a base class for more complex widgets.

    Parameters Parameter Default Description renderable RenderableType ''

    A Rich renderable, or string containing console markup.

    expand bool False

    Expand content if required to fill container.

    shrink bool False

    Shrink content if required to fill container.

    markup bool True

    True if markup should be parsed and rendered.

    name str | None None

    Name of widget.

    id str | None None

    ID of Widget.

    classes str | None None

    Space separated list of class names.

    disabled bool False

    Whether the static is disabled or not.

    "},{"location":"widgets/static/#textual.widgets._static.Static.update","title":"update method","text":"
    def update(self, renderable=''):\n

    Update the widget's content area with new text or Rich renderable.

    Parameters Parameter Default Description renderable RenderableType ''

    A new rich renderable. Defaults to empty renderable;

    "},{"location":"widgets/switch/","title":"Switch","text":"

    A simple switch widget which stores a boolean value.

    • Focusable
    • Container
    "},{"location":"widgets/switch/#example","title":"Example","text":"

    The example below shows switches in various states.

    Outputswitch.pyswitch.tcss

    SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static, Switch\n\n\nclass SwitchApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(\"[b]Example switches\\n\", classes=\"label\")\n        yield Horizontal(\n            Static(\"off:     \", classes=\"label\"),\n            Switch(animate=False),\n            classes=\"container\",\n        )\n        yield Horizontal(\n            Static(\"on:      \", classes=\"label\"),\n            Switch(value=True),\n            classes=\"container\",\n        )\n\n        focused_switch = Switch()\n        focused_switch.focus()\n        yield Horizontal(\n            Static(\"focused: \", classes=\"label\"), focused_switch, classes=\"container\"\n        )\n\n        yield Horizontal(\n            Static(\"custom:  \", classes=\"label\"),\n            Switch(id=\"custom-design\"),\n            classes=\"container\",\n        )\n\n\napp = SwitchApp(css_path=\"switch.tcss\")\nif __name__ == \"__main__\":\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\n.container {\n    height: auto;\n    width: auto;\n}\n\nSwitch {\n    height: auto;\n    width: auto;\n}\n\n.label {\n    height: 3;\n    content-align: center middle;\n    width: auto;\n}\n\n#custom-design {\n    background: darkslategrey;\n}\n\n#custom-design > .switch--slider {\n    color: dodgerblue;\n    background: darkslateblue;\n}\n
    "},{"location":"widgets/switch/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the switch."},{"location":"widgets/switch/#messages","title":"Messages","text":"
    • Switch.Changed
    "},{"location":"widgets/switch/#bindings","title":"Bindings","text":"

    The switch widget defines the following bindings:

    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#component-classes","title":"Component Classes","text":"

    The switch widget provides the following component classes:

    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#additional-notes","title":"Additional Notes","text":"
    • To remove the spacing around a Switch, set border: none; and padding: 0;.
    "},{"location":"widgets/switch/#textual.widgets.Switch","title":"textual.widgets.Switch class","text":"
    def __init__(\n    self,\n    value=False,\n    *,\n    animate=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A switch widget that represents a boolean value.

    Can be toggled by clicking on it or through its bindings.

    The switch widget also contains component classes that enable more customization.

    Parameters Parameter Default Description value bool False

    The initial value of the switch.

    animate bool True

    True if the switch should animate when toggled.

    name str | None None

    The name of the switch.

    id str | None None

    The ID of the switch in the DOM.

    classes str | None None

    The CSS classes of the switch.

    disabled bool False

    Whether the switch is disabled or not.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False)\n]\n
    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets._switch.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {'switch--slider'}\n
    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets._switch.Switch.slider_pos","title":"slider_pos instance-attribute class-attribute","text":"
    slider_pos = reactive(0.0)\n

    The position of the slider.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.value","title":"value instance-attribute class-attribute","text":"
    value = reactive(False, init=False)\n

    The value of the switch; True for on and False for off.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed","title":"Changed class","text":"
    def __init__(self, switch, value):\n

    Bases: Message

    Posted when the status of the switch changes.

    Can be handled using on_switch_changed in a subclass of Switch or in a parent widget in the DOM.

    Attributes Name Type Description value bool

    The value that the switch was changed to.

    switch Switch

    The Switch widget that was changed.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed.control","title":"control property","text":"
    control: Switch\n

    Alias for self.switch.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.action_toggle","title":"action_toggle method","text":"
    def action_toggle(self):\n

    Toggle the state of the switch.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.toggle","title":"toggle method","text":"
    def toggle(self):\n

    Toggle the switch value.

    As a result of the value changing, a Switch.Changed message will be posted.

    Returns Type Description Self

    The Switch instance.

    "},{"location":"widgets/tabbed_content/","title":"TabbedContent","text":"

    Added in version 0.16.0

    Switch between mutually exclusive content panes via a row of tabs.

    • Focusable
    • Container

    This widget combines the Tabs and ContentSwitcher widgets to create a convenient way of navigating content.

    Only a single child of TabbedContent is visible at once. Each child has an associated tab which will make it visible and hide the others.

    "},{"location":"widgets/tabbed_content/#composing","title":"Composing","text":"

    There are two ways to provide the titles for the tab. You can pass them as positional arguments to the TabbedContent constructor:

    def compose(self) -> ComposeResult:\n    with TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\n        yield Markdown(LETO)\n        yield Markdown(JESSICA)\n        yield Markdown(PAUL)\n

    Alternatively you can wrap the content in a TabPane widget, which takes the tab title as the first parameter:

    def compose(self) -> ComposeResult:\n    with TabbedContent():\n        with TabPane(\"Leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\"):\n            yield Markdown(PAUL)\n
    "},{"location":"widgets/tabbed_content/#switching-tabs","title":"Switching tabs","text":"

    If you need to programmatically switch tabs, you should provide an id attribute to the TabPanes.

    def compose(self) -> ComposeResult:\n    with TabbedContent():\n        with TabPane(\"Leto\", id=\"leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\", id=\"jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\", id=\"paul\"):\n            yield Markdown(PAUL)\n

    You can then switch tabs by setting the active reactive attribute:

    # Switch to Jessica tab\nself.query_one(TabbedContent).active = \"jessica\"\n

    Note

    If you don't provide id attributes to the tab panes, they will be assigned sequentially starting at tab-1 (then tab-2 etc).

    "},{"location":"widgets/tabbed_content/#initial-tab","title":"Initial tab","text":"

    The first child of TabbedContent will be the initial active tab by default. You can pick a different initial tab by setting the initial argument to the id of the tab:

    def compose(self) -> ComposeResult:\n    with TabbedContent(initial=\"jessica\"):\n        with TabPane(\"Leto\", id=\"leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\", id=\"jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\", id=\"paul\"):\n            yield Markdown(PAUL)\n
    "},{"location":"widgets/tabbed_content/#example","title":"Example","text":"

    The following example contains a TabbedContent with three tabs.

    Outputtabbed_content.py

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Markdown, TabbedContent, TabPane\n\nLETO = \"\"\"\n# Duke Leto I Atreides\n\nHead of House Atreides.\n\"\"\"\n\nJESSICA = \"\"\"\n# Lady Jessica\n\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\n\nPAUL = \"\"\"\n# Paul Atreides\n\nSon of Leto and Jessica.\n\"\"\"\n\n\nclass TabbedApp(App):\n    \"\"\"An example of tabbed content.\"\"\"\n\n    BINDINGS = [\n        (\"l\", \"show_tab('leto')\", \"Leto\"),\n        (\"j\", \"show_tab('jessica')\", \"Jessica\"),\n        (\"p\", \"show_tab('paul')\", \"Paul\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Compose app with tabbed content.\"\"\"\n        # Footer to show keys\n        yield Footer()\n\n        # Add the TabbedContent widget\n        with TabbedContent(initial=\"jessica\"):\n            with TabPane(\"Leto\", id=\"leto\"):  # First tab\n                yield Markdown(LETO)  # Tab content\n            with TabPane(\"Jessica\", id=\"jessica\"):\n                yield Markdown(JESSICA)\n                with TabbedContent(\"Paul\", \"Alia\"):\n                    yield TabPane(\"Paul\", Label(\"First child\"))\n                    yield TabPane(\"Alia\", Label(\"Second child\"))\n\n            with TabPane(\"Paul\", id=\"paul\"):\n                yield Markdown(PAUL)\n\n    def action_show_tab(self, tab: str) -> None:\n        \"\"\"Switch to a new tab.\"\"\"\n        self.get_child_by_type(TabbedContent).active = tab\n\n\nif __name__ == \"__main__\":\n    app = TabbedApp()\n    app.run()\n
    "},{"location":"widgets/tabbed_content/#styling","title":"Styling","text":"

    The TabbedContent widget is composed of two main sub-widgets: a Tabs and a ContentSwitcher; you can style them accordingly.

    The tabs within the Tabs widget will have prefixed IDs; each ID being the ID of the TabPane the Tab is for, prefixed with --content-tab-. If you wish to style individual tabs within the TabbedContent widget you will need to use that prefix for the Tab IDs.

    For example, to create a TabbedContent that has red and green labels:

    Outputtabbed_content.py

    ColorTabsApp RedGreen \u2501\u2578\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Red!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, TabbedContent, TabPane\n\n\nclass ColorTabsApp(App):\n    CSS = \"\"\"\n    TabbedContent #--content-tab-green {\n        color: green;\n    }\n\n    TabbedContent #--content-tab-red {\n        color: red;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with TabbedContent():\n            with TabPane(\"Red\", id=\"red\"):\n                yield Label(\"Red!\")\n            with TabPane(\"Green\", id=\"green\"):\n                yield Label(\"Green!\")\n\n\nif __name__ == \"__main__\":\n    ColorTabsApp().run()\n
    "},{"location":"widgets/tabbed_content/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The id attribute of the active tab. Set this to switch tabs."},{"location":"widgets/tabbed_content/#messages","title":"Messages","text":"
    • TabbedContent.TabActivated
    "},{"location":"widgets/tabbed_content/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/tabbed_content/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/tabbed_content/#see-also","title":"See also","text":"
    • Tabs
    • ContentSwitcher
    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent","title":"textual.widgets.TabbedContent class","text":"
    def __init__(\n    self,\n    *titles,\n    initial=\"\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A container with associated tabs to toggle content visibility.

    Parameters Parameter Default Description *titles TextType ()

    Positional argument will be used as title.

    initial str ''

    The id of the initial tab, or empty string to select the first tab.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.active","title":"active instance-attribute class-attribute","text":"
    active: reactive[str] = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.tab_count","title":"tab_count property","text":"
    tab_count: int\n

    Total number of tabs.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared","title":"Cleared class","text":"
    def __init__(self, tabbed_content):\n

    Bases: Message

    Posted when there are no more tab panes.

    Parameters Parameter Default Description tabbed_content TabbedContent required

    The TabbedContent widget.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.control","title":"control property","text":"
    control: TabbedContent\n

    The TabbedContent widget that was cleared of all tab panes.

    This is an alias for Cleared.tabbed_content and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.tabbed_content","title":"tabbed_content instance-attribute","text":"
    tabbed_content = tabbed_content\n

    The TabbedContent widget that contains the tab activated.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated","title":"TabActivated class","text":"
    def __init__(self, tabbed_content, tab):\n

    Bases: Message

    Posted when the active tab changes.

    Parameters Parameter Default Description tabbed_content TabbedContent required

    The TabbedContent widget.

    tab ContentTab required

    The Tab widget that was selected (contains the tab label).

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'pane'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.control","title":"control property","text":"
    control: TabbedContent\n

    The TabbedContent widget that contains the tab activated.

    This is an alias for TabActivated.tabbed_content and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.pane","title":"pane instance-attribute","text":"
    pane = tabbed_content.get_pane(tab)\n

    The TabPane widget that was activated by selecting the tab.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tab","title":"tab instance-attribute","text":"
    tab = tab\n

    The Tab widget that was selected (contains the tab label).

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tabbed_content","title":"tabbed_content instance-attribute","text":"
    tabbed_content = tabbed_content\n

    The TabbedContent widget that contains the tab activated.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.add_pane","title":"add_pane method","text":"
    def add_pane(self, pane, *, before=None, after=None):\n

    Add a new pane to the tabbed content.

    Parameters Parameter Default Description pane TabPane required

    The pane to add.

    before TabPane | str | None None

    Optional pane or pane ID to add the pane before.

    after TabPane | str | None None

    Optional pane or pane ID to add the pane after.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be added.

    Raises Type Description Tabs.TabError

    If there is a problem with the addition request.

    Note

    Only one of before or after can be provided. If both are provided an exception is raised.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.clear_panes","title":"clear_panes method","text":"
    def clear_panes(self):\n

    Remove all the panes in the tabbed content.

    Returns Type Description AwaitComplete

    An optionally awaitable object which waits for all panes to be removed and the Cleared message to be posted.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.disable_tab","title":"disable_tab method","text":"
    def disable_tab(self, tab_id):\n

    Disables the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to disable.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.enable_tab","title":"enable_tab method","text":"
    def enable_tab(self, tab_id):\n

    Enables the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to enable.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.get_pane","title":"get_pane method","text":"
    def get_pane(self, pane_id):\n

    Get the TabPane associated with the given ID or tab.

    Parameters Parameter Default Description pane_id str | ContentTab required

    The ID of the pane to get, or the Tab it is associated with.

    Returns Type Description TabPane

    The TabPane associated with the ID or the given tab.

    Raises Type Description ValueError

    Raised if no ID was available.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.get_tab","title":"get_tab method","text":"
    def get_tab(self, pane_id):\n

    Get the Tab associated with the given ID or TabPane.

    Parameters Parameter Default Description pane_id str | TabPane required

    The ID of the pane, or the pane itself.

    Returns Type Description Tab

    The Tab associated with the ID.

    Raises Type Description ValueError

    Raised if no ID was available.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.hide_tab","title":"hide_tab method","text":"
    def hide_tab(self, tab_id):\n

    Hides the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to hide.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.remove_pane","title":"remove_pane method","text":"
    def remove_pane(self, pane_id):\n

    Remove a given pane from the tabbed content.

    Parameters Parameter Default Description pane_id str required

    The ID of the pane to remove.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be removed and the Cleared message to be posted.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.show_tab","title":"show_tab method","text":"
    def show_tab(self, tab_id):\n

    Shows the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to show.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.validate_active","title":"validate_active method","text":"
    def validate_active(self, active):\n

    It doesn't make sense for active to be an empty string.

    Parameters Parameter Default Description active str required

    Attribute to be validated.

    Returns Type Description str

    Value of active.

    Raises Type Description ValueError

    If the active attribute is set to empty string when there are tabs available.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane","title":"textual.widgets.TabPane class","text":"
    def __init__(\n    self,\n    title,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A container for switchable content, with additional title.

    This widget is intended to be used with TabbedContent.

    Parameters Parameter Default Description title TextType required

    Title of the TabPane (will be displayed in a tab label).

    *children Widget ()

    Widget to go inside the TabPane.

    name str | None None

    Optional name for the TabPane.

    id str | None None

    Optional ID for the TabPane.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the TabPane is disabled or not.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Disabled","title":"Disabled class","text":"

    Bases: TabPaneMessage

    Sent when a tab pane is disabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Enabled","title":"Enabled class","text":"

    Bases: TabPaneMessage

    Sent when a tab pane is enabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage","title":"TabPaneMessage class","text":"

    Bases: Message

    Base class for TabPane messages.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.control","title":"control property","text":"
    control: TabPane\n

    The tab pane that is the object of this message.

    This is an alias for the attribute tab_pane and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
    tab_pane: TabPane\n

    The TabPane that is he object of this message.

    "},{"location":"widgets/tabs/","title":"Tabs","text":"

    Added in version 0.15.0

    Displays a number of tab headers which may be activated with a click or navigated with cursor keys.

    • Focusable
    • Container

    Construct a Tabs widget with strings or Text objects as positional arguments, which will set the labels in the tabs. Here's an example with three tabs:

    def compose(self) -> ComposeResult:\n    yield Tabs(\"First tab\", \"Second tab\", Text.from_markup(\"[u]Third[/u] tab\"))\n

    This will create Tab widgets internally, with auto-incrementing id attributes (\"tab-1\", \"tab-2\" etc). You can also supply Tab objects directly in the constructor, which will allow you to explicitly set an id. Here's an example:

    def compose(self) -> ComposeResult:\n    yield Tabs(\n        Tab(\"First tab\", id=\"one\"),\n        Tab(\"Second tab\", id=\"two\"),\n    )\n

    When the user switches to a tab by clicking or pressing keys, then Tabs will send a Tabs.TabActivated message which contains the tab that was activated. You can then use event.tab.id attribute to perform any related actions.

    "},{"location":"widgets/tabs/#clearing-tabs","title":"Clearing tabs","text":"

    Clear tabs by calling the clear method. Clearing the tabs will send a Tabs.TabActivated message with the tab attribute set to None.

    "},{"location":"widgets/tabs/#adding-tabs","title":"Adding tabs","text":"

    Tabs may be added dynamically with the add_tab method, which accepts strings, Text, or Tab objects.

    "},{"location":"widgets/tabs/#example","title":"Example","text":"

    The following example adds a Tabs widget above a text label. Press A to add a tab, C to clear the tabs.

    Outputtabs.py

    TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Tabs\n\nNAMES = [\n    \"Paul Atreidies\",\n    \"Duke Leto Atreides\",\n    \"Lady Jessica\",\n    \"Gurney Halleck\",\n    \"Baron Vladimir Harkonnen\",\n    \"Glossu Rabban\",\n    \"Chani\",\n    \"Silgar\",\n]\n\n\nclass TabsApp(App):\n    \"\"\"Demonstrates the Tabs widget.\"\"\"\n\n    CSS = \"\"\"\n    Tabs {\n        dock: top;\n    }\n    Screen {\n        align: center middle;\n    }\n    Label {\n        margin:1 1;\n        width: 100%;\n        height: 100%;\n        background: $panel;\n        border: tall $primary;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    BINDINGS = [\n        (\"a\", \"add\", \"Add tab\"),\n        (\"r\", \"remove\", \"Remove active tab\"),\n        (\"c\", \"clear\", \"Clear tabs\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Tabs(NAMES[0])\n        yield Label()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Focus the tabs when the app starts.\"\"\"\n        self.query_one(Tabs).focus()\n\n    def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:\n        \"\"\"Handle TabActivated message sent by Tabs.\"\"\"\n        label = self.query_one(Label)\n        if event.tab is None:\n            # When the tabs are cleared, event.tab will be None\n            label.visible = False\n        else:\n            label.visible = True\n            label.update(event.tab.label)\n\n    def action_add(self) -> None:\n        \"\"\"Add a new tab.\"\"\"\n        tabs = self.query_one(Tabs)\n        # Cycle the names\n        NAMES[:] = [*NAMES[1:], NAMES[0]]\n        tabs.add_tab(NAMES[0])\n\n    def action_remove(self) -> None:\n        \"\"\"Remove active tab.\"\"\"\n        tabs = self.query_one(Tabs)\n        active_tab = tabs.active_tab\n        if active_tab is not None:\n            tabs.remove_tab(active_tab.id)\n\n    def action_clear(self) -> None:\n        \"\"\"Clear the tabs.\"\"\"\n        self.query_one(Tabs).clear()\n\n\nif __name__ == \"__main__\":\n    app = TabsApp()\n    app.run()\n
    "},{"location":"widgets/tabs/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The ID of the active tab. Set this attribute to a tab ID to change the active tab."},{"location":"widgets/tabs/#messages","title":"Messages","text":"
    • Tabs.TabActivated
    • Tabs.Cleared
    "},{"location":"widgets/tabs/#bindings","title":"Bindings","text":"

    The Tabs widget defines the following bindings:

    Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs","title":"textual.widgets.Tabs class","text":"
    def __init__(\n    self,\n    *tabs,\n    active=None,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A row of tabs.

    Parameters Parameter Default Description *tabs Tab | TextType ()

    Positional argument should be explicit Tab objects, or a str or Text.

    active str | None None

    ID of the tab which should be active on start.

    name str | None None

    Optional name for the input widget.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the input is disabled or not.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"left\", \"previous_tab\", \"Previous tab\", show=False\n    ),\n    Binding(\"right\", \"next_tab\", \"Next tab\", show=False),\n]\n
    Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active","title":"active instance-attribute class-attribute","text":"
    active: reactive[str] = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active_tab","title":"active_tab property","text":"
    active_tab: Tab | None\n

    The currently active tab, or None if there are no active tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.tab_count","title":"tab_count property","text":"
    tab_count: int\n

    Total number of tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared","title":"Cleared class","text":"
    def __init__(self, tabs):\n

    Bases: Message

    Sent when there are no active tabs.

    This can occur when Tabs are cleared, or if all tabs are hidden.

    Parameters Parameter Default Description tabs Tabs required

    The tabs widget.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.control","title":"control property","text":"
    control: Tabs\n

    The tabs widget which was cleared.

    This is an alias for Cleared.tabs which is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.tabs","title":"tabs instance-attribute","text":"
    tabs: Tabs = tabs\n

    The tabs widget which was cleared.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabActivated","title":"TabActivated class","text":"

    Bases: TabMessage

    Sent when a new tab is activated.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabDisabled","title":"TabDisabled class","text":"

    Bases: TabMessage

    Sent when a tab is disabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabEnabled","title":"TabEnabled class","text":"

    Bases: TabMessage

    Sent when a tab is enabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabError","title":"TabError class","text":"

    Bases: Exception

    Exception raised when there is an error relating to tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabHidden","title":"TabHidden class","text":"

    Bases: TabMessage

    Sent when a tab is hidden.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage","title":"TabMessage class","text":"
    def __init__(self, tabs, tab):\n

    Bases: Message

    Parent class for all messages that have to do with a specific tab.

    Parameters Parameter Default Description tabs Tabs required

    The Tabs widget.

    tab Tab required

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'tab'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.control","title":"control property","text":"
    control: Tabs\n

    The tabs widget containing the tab that is the object of this message.

    This is an alias for the attribute tabs and is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tab","title":"tab instance-attribute","text":"
    tab: Tab = tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
    tabs: Tabs = tabs\n

    The tabs widget containing the tab.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabShown","title":"TabShown class","text":"

    Bases: TabMessage

    Sent when a tab is shown.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_next_tab","title":"action_next_tab method","text":"
    def action_next_tab(self):\n

    Make the next tab active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_previous_tab","title":"action_previous_tab method","text":"
    def action_previous_tab(self):\n

    Make the previous tab active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.add_tab","title":"add_tab method","text":"
    def add_tab(self, tab, *, before=None, after=None):\n

    Add a new tab to the end of the tab list.

    Parameters Parameter Default Description tab Tab | str | Text required

    A new tab object, or a label (str or Text).

    before Tab | str | None None

    Optional tab or tab ID to add the tab before.

    after Tab | str | None None

    Optional tab or tab ID to add the tab after.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be mounted and internal state to be fully updated to reflect the new tab.

    Raises Type Description Tabs.TabError

    If there is a problem with the addition request.

    Note

    Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all the tabs.

    Returns Type Description AwaitComplete

    An awaitable object that waits for the tabs to be removed.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.disable","title":"disable method","text":"
    def disable(self, tab_id):\n

    Disable the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to disable.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.enable","title":"enable method","text":"
    def enable(self, tab_id):\n

    Enable the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to enable.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.hide","title":"hide method","text":"
    def hide(self, tab_id):\n

    Hide the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to hide.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.remove_tab","title":"remove_tab method","text":"
    def remove_tab(self, tab_or_id):\n

    Remove a tab.

    Parameters Parameter Default Description tab_or_id Tab | str | None required

    The Tab to remove or its id.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be removed.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.show","title":"show method","text":"
    def show(self, tab_id):\n

    Show the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to show.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.validate_active","title":"validate_active method","text":"
    def validate_active(self, active):\n

    Check id assigned to active attribute is a valid tab.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.watch_active","title":"watch_active method","text":"
    def watch_active(self, previously_active, active):\n

    Handle a change to the active tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab","title":"textual.widgets.Tab class","text":"
    def __init__(\n    self, label, *, id=None, classes=None, disabled=False\n):\n

    Bases: Static

    A Widget to manage a single tab within a Tabs widget.

    Parameters Parameter Default Description label TextType required

    The label to use in the tab.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Space separated list of class names.

    disabled bool False

    Whether the tab is disabled or not.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.label_text","title":"label_text property","text":"
    label_text: str\n

    Undecorated text of the label.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Clicked","title":"Clicked class","text":"

    Bases: TabMessage

    A tab was clicked.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Disabled","title":"Disabled class","text":"

    Bases: TabMessage

    A tab was disabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Enabled","title":"Enabled class","text":"

    Bases: TabMessage

    A tab was enabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage","title":"TabMessage class","text":"

    Bases: Message

    Tab-related messages.

    These are mostly intended for internal use when interacting with Tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.control","title":"control property","text":"
    control: Tab\n

    The tab that is the object of this message.

    This is an alias for the attribute tab and is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
    tab: Tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/text_area/","title":"TextArea","text":"

    Added in version 0.38.0

    A widget for editing text which may span multiple lines. Supports syntax highlighting for a selection of languages.

    • Focusable
    • Container
    "},{"location":"widgets/text_area/#guide","title":"Guide","text":""},{"location":"widgets/text_area/#syntax-highlighting-dependencies","title":"Syntax highlighting dependencies","text":"

    To enable syntax highlighting, you'll need to install the syntax extra dependencies:

    pippoetry
    pip install \"textual[syntax]\"\n
    poetry add \"textual[syntax]\"\n

    This will install tree-sitter and tree-sitter-languages. These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not supported.

    "},{"location":"widgets/text_area/#loading-text","title":"Loading text","text":"

    In this example we load some initial text into the TextArea, and set the language to \"python\" to enable syntax highlighting.

    Outputtext_area_example.py

    TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\n\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\n\n\nclass TextAreaExample(App):\n    def compose(self) -> ComposeResult:\n        yield TextArea(TEXT, language=\"python\")\n\n\napp = TextAreaExample()\nif __name__ == \"__main__\":\n    app.run()\n

    To load content into the TextArea after it has already been created, use the load_text method.

    To update the parser used for syntax highlighting, set the language reactive attribute:

    # Set the language to Markdown\ntext_area.language = \"markdown\"\n

    Note

    More built-in languages will be added in the future. For now, you can add your own.

    "},{"location":"widgets/text_area/#reading-content-from-textarea","title":"Reading content from TextArea","text":"

    There are a number of ways to retrieve content from the TextArea:

    • The TextArea.text property returns all content in the text area as a string.
    • The TextArea.selected_text property returns the text corresponding to the current selection.
    • The TextArea.get_text_range method returns the text between two locations.

    In all cases, when multiple lines of text are retrieved, the document line separator will be used.

    "},{"location":"widgets/text_area/#editing-content-inside-textarea","title":"Editing content inside TextArea","text":"

    The content of the TextArea can be updated using the replace method. This method is the programmatic equivalent of selecting some text and then pasting.

    Some other convenient methods are available, such as insert, delete, and clear.

    "},{"location":"widgets/text_area/#working-with-the-cursor","title":"Working with the cursor","text":""},{"location":"widgets/text_area/#moving-the-cursor","title":"Moving the cursor","text":"

    The cursor location is available via the cursor_location property, which represents the location of the cursor as a tuple (row_index, column_index). These indices are zero-based. Writing a new value to cursor_location will immediately update the location of the cursor.

    >>> text_area = TextArea()\n>>> text_area.cursor_location\n(0, 0)\n>>> text_area.cursor_location = (0, 4)\n>>> text_area.cursor_location\n(0, 4)\n

    cursor_location is a simple way to move the cursor programmatically, but it doesn't let us select text.

    "},{"location":"widgets/text_area/#selecting-text","title":"Selecting text","text":"

    To select text, we can use the selection reactive attribute. Let's select the first two lines of text in a document by adding text_area.selection = Selection(start=(0, 0), end=(2, 0)) to our code:

    Outputtext_area_selection.py

    TextAreaSelection 1\u00a0\u00a0defhello(name): 2\u00a0\u00a0print(\"hello\"+\u00a0name) 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nfrom textual.widgets.text_area import Selection\n\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\n\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\n\n\nclass TextAreaSelection(App):\n    def compose(self) -> ComposeResult:\n        text_area = TextArea(TEXT, language=\"python\")\n        text_area.selection = Selection(start=(0, 0), end=(2, 0))  # (1)!\n        yield text_area\n\n\napp = TextAreaSelection()\nif __name__ == \"__main__\":\n    app.run()\n
    1. Selects the first two lines of text.

    Note that selections can happen in both directions, so Selection((2, 0), (0, 0)) is also valid.

    Tip

    The end attribute of the selection is always equal to TextArea.cursor_location. In other words, the cursor_location attribute is simply a convenience for accessing text_area.selection.end.

    "},{"location":"widgets/text_area/#more-cursor-utilities","title":"More cursor utilities","text":"

    There are a number of additional utility methods available for interacting with the cursor.

    "},{"location":"widgets/text_area/#location-information","title":"Location information","text":"

    A number of properties exist on TextArea which give information about the current cursor location. These properties begin with cursor_at_, and return booleans. For example, cursor_at_start_of_line tells us if the cursor is at a start of line.

    We can also check the location the cursor would arrive at if we were to move it. For example, get_cursor_right_location returns the location the cursor would move to if it were to move right. A number of similar methods exist, with names like get_cursor_*_location.

    "},{"location":"widgets/text_area/#cursor-movement-methods","title":"Cursor movement methods","text":"

    The move_cursor method allows you to move the cursor to a new location while selecting text, or move the cursor and scroll to keep it centered.

    # Move the cursor from its current location to row index 4,\n# column index 8, while selecting all the text between.\ntext_area.move_cursor((4, 8), select=True)\n

    The move_cursor_relative method offers a very similar interface, but moves the cursor relative to its current location.

    "},{"location":"widgets/text_area/#common-selections","title":"Common selections","text":"

    There are some methods available which make common selections easier:

    • select_line selects a line by index. Bound to F6 by default.
    • select_all selects all text. Bound to F7 by default.
    "},{"location":"widgets/text_area/#themes","title":"Themes","text":"

    TextArea ships with some builtin themes, and you can easily add your own.

    Themes give you control over the look and feel, including syntax highlighting, the cursor, selection, gutter, and more.

    "},{"location":"widgets/text_area/#using-builtin-themes","title":"Using builtin themes","text":"

    The initial theme of the TextArea is determined by the theme parameter.

    # Create a TextArea with the 'dracula' theme.\nyield TextArea(\"print(123)\", language=\"python\", theme=\"dracula\")\n

    You can check which themes are available using the available_themes property.

    >>> text_area = TextArea()\n>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark'}\n

    After creating a TextArea, you can change the theme by setting the theme attribute to one of the available themes.

    text_area.theme = \"vscode_dark\"\n

    On setting this attribute the TextArea will immediately refresh to display the updated theme.

    "},{"location":"widgets/text_area/#custom-themes","title":"Custom themes","text":"

    Using custom (non-builtin) themes is two-step process:

    1. Create an instance of TextAreaTheme.
    2. Register it using TextArea.register_theme.
    "},{"location":"widgets/text_area/#1-creating-a-theme","title":"1. Creating a theme","text":"

    Let's create a simple theme, \"my_cool_theme\", which colors the cursor blue, and the cursor line yellow. Our theme will also syntax highlight strings as red, and comments as magenta.

    from rich.style import Style\nfrom textual.widgets.text_area import TextAreaTheme\n# ...\nmy_theme = TextAreaTheme(\n    # This name will be used to refer to the theme...\n    name=\"my_cool_theme\",\n    # Basic styles such as background, cursor, selection, gutter, etc...\n    cursor_style=Style(color=\"white\", bgcolor=\"blue\"),\n    cursor_line_style=Style(bgcolor=\"yellow\"),\n    # `syntax_styles` is for syntax highlighting.\n    # It maps tokens parsed from the document to Rich styles.\n    syntax_styles={\n        \"string\": Style(color=\"red\"),\n        \"comment\": Style(color=\"magenta\"),\n    }\n)\n

    Attributes like cursor_style and cursor_line_style apply general language-agnostic styling to the widget.

    The syntax_styles attribute of TextAreaTheme is used for syntax highlighting and depends on the language currently in use. For more details, see syntax highlighting.

    If you wish to build on an existing theme, you can obtain a reference to it using the TextAreaTheme.get_builtin_theme classmethod:

    from textual.widgets.text_area import TextAreaTheme\n\nmonokai = TextAreaTheme.get_builtin_theme(\"monokai\")\n
    "},{"location":"widgets/text_area/#2-registering-a-theme","title":"2. Registering a theme","text":"

    Our theme can now be registered with the TextArea instance.

    text_area.register_theme(my_theme)\n

    After registering a theme, it'll appear in the available_themes:

    >>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}\n

    We can now switch to it:

    text_area.theme = \"my_cool_theme\"\n

    This immediately updates the appearance of the TextArea:

    TextAreaCustomThemes 1\u00a0\u00a0#\u00a0says\u00a0hello 2\u00a0\u00a0def\u00a0hello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u00a0\u00a0 4\u00a0\u00a0 5\u00a0\u00a0#\u00a0says\u00a0goodbye 6\u00a0\u00a0def\u00a0goodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 7\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0 8\u00a0\u00a0

    "},{"location":"widgets/text_area/#indentation","title":"Indentation","text":"

    The character(s) inserted when you press tab is controlled by setting the indent_type attribute to either tabs or spaces.

    If indent_type == \"spaces\", pressing Tab will insert up to indent_width spaces in order to align with the next tab stop.

    "},{"location":"widgets/text_area/#line-separators","title":"Line separators","text":"

    When content is loaded into TextArea, the content is scanned from beginning to end and the first occurrence of a line separator is recorded.

    This separator will then be used when content is later read from the TextArea via the text property. The TextArea widget does not support exporting text which contains mixed line endings.

    Similarly, newline characters pasted into the TextArea will be converted.

    You can check the line separator of the current document by inspecting TextArea.document.newline:

    >>> text_area = TextArea()\n>>> text_area.document.newline\n'\\n'\n
    "},{"location":"widgets/text_area/#line-numbers","title":"Line numbers","text":"

    The gutter (column on the left containing line numbers) can be toggled by setting the show_line_numbers attribute to True or False.

    Setting this attribute will immediately repaint the TextArea to reflect the new value.

    "},{"location":"widgets/text_area/#extending-textarea","title":"Extending TextArea","text":"

    Sometimes, you may wish to subclass TextArea to add some extra functionality. In this section, we'll briefly explore how we can extend the widget to achieve common goals.

    "},{"location":"widgets/text_area/#hooking-into-key-presses","title":"Hooking into key presses","text":"

    You may wish to hook into certain key presses to inject some functionality. This can be done by over-riding _on_key and adding the required functionality.

    "},{"location":"widgets/text_area/#example-closing-parentheses-automatically","title":"Example - closing parentheses automatically","text":"

    Let's extend TextArea to add a feature which automatically closes parentheses and moves the cursor to a sensible location.

    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\n\nclass ExtendedTextArea(TextArea):\n    \"\"\"A subclass of TextArea with parenthesis-closing functionality.\"\"\"\n\n    def _on_key(self, event: events.Key) -> None:\n        if event.character == \"(\":\n            self.insert(\"()\")\n            self.move_cursor_relative(columns=-1)\n            event.prevent_default()\n\n\nclass TextAreaKeyPressHook(App):\n    def compose(self) -> ComposeResult:\n        yield ExtendedTextArea(language=\"python\")\n\n\napp = TextAreaKeyPressHook()\nif __name__ == \"__main__\":\n    app.run()\n

    This intercepts the key handler when \"(\" is pressed, and inserts \"()\" instead. It then moves the cursor so that it lands between the open and closing parentheses.

    Typing def hello( into the TextArea results in the bracket automatically being closed:

    TextAreaKeyPressHook 1\u00a0\u00a0def\u00a0hello()

    "},{"location":"widgets/text_area/#advanced-concepts","title":"Advanced concepts","text":""},{"location":"widgets/text_area/#syntax-highlighting","title":"Syntax highlighting","text":"

    Syntax highlighting inside the TextArea is powered by a library called tree-sitter.

    Each time you update the document in a TextArea, an internal syntax tree is updated. This tree is frequently queried to find location ranges relevant to syntax highlighting. We give these ranges names, and ultimately map them to Rich styles inside TextAreaTheme.syntax_styles.

    To illustrate how this works, lets look at how the \"Monokai\" TextAreaTheme highlights Markdown files.

    When the language attribute is set to \"markdown\", a highlight query similar to the one below is used (trimmed for brevity).

    (heading_content) @heading\n(link) @link\n

    This highlight query maps heading_content nodes returned by the Markdown parser to the name @heading, and link nodes to the name @link.

    Inside our TextAreaTheme.syntax_styles dict, we can map the name @heading to a Rich style. Here's a snippet from the \"Monokai\" theme which does just that:

    TextAreaTheme(\n    name=\"monokai\",\n    base_style=Style(color=\"#f8f8f2\", bgcolor=\"#272822\"),\n    gutter_style=Style(color=\"#90908a\", bgcolor=\"#272822\"),\n    # ...\n    syntax_styles={\n        # Colorise @heading and make them bold\n        \"heading\": Style(color=\"#F92672\", bold=True),\n        # Colorise and underline @link\n        \"link\": Style(color=\"#66D9EF\", underline=True),\n        # ...\n    },\n)\n

    To understand which names can be mapped inside syntax_styles, we recommend looking at the existing themes and highlighting queries (.scm files) in the Textual repository.

    Tip

    You may also wish to take a look at the contents of TextArea._highlights on an active TextArea instance to see which highlights have been generated for the open document.

    "},{"location":"widgets/text_area/#adding-support-for-custom-languages","title":"Adding support for custom languages","text":"

    To add support for a language to a TextArea, use the register_language method.

    To register a language, we require two things:

    1. A tree-sitter Language object which contains the grammar for the language.
    2. A highlight query which is used for syntax highlighting.
    "},{"location":"widgets/text_area/#example-adding-java-support","title":"Example - adding Java support","text":"

    The easiest way to obtain a Language object is using the py-tree-sitter-languages package. Here's how we can use this package to obtain a reference to a Language object representing Java:

    from tree_sitter_languages import get_language\njava_language = get_language(\"java\")\n

    The exact version of the parser used when you call get_language can be checked via the repos.txt file in the version of py-tree-sitter-languages you're using. This file contains links to the GitHub repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at queries/highlights.scm, and a file showing all the available node types which can be used in highlight queries at src/node-types.json.

    Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps:

    1. Open repos.txt file from the py-tree-sitter-languages repo.
    2. Find the link corresponding to tree-sitter-java and go to the repo on GitHub (you may also need to go to the specific commit referenced in repos.txt).
    3. Go to queries/highlights.scm to see the example highlight query for Java.

    Be sure to check the license in the repo to ensure it can be freely copied.

    Warning

    It's important to use a highlight query which is compatible with the parser in use, so pay attention to the commit hash when visiting the repo via repos.txt.

    We now have our Language and our highlight query, so we can register Java as a language.

    from pathlib import Path\n\nfrom tree_sitter_languages import get_language\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\njava_language = get_language(\"java\")\njava_highlight_query = (Path(__file__).parent / \"java_highlights.scm\").read_text()\njava_code = \"\"\"\\\nclass HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, World!\");\n    }\n}\n\"\"\"\n\n\nclass TextAreaCustomLanguage(App):\n    def compose(self) -> ComposeResult:\n        text_area = TextArea(text=java_code)\n        text_area.cursor_blink = False\n\n        # Register the Java language and highlight query\n        text_area.register_language(java_language, java_highlight_query)\n\n        # Switch to Java\n        text_area.language = \"java\"\n        yield text_area\n\n\napp = TextAreaCustomLanguage()\nif __name__ == \"__main__\":\n    app.run()\n

    Running our app, we can see that the Java code is highlighted. We can freely edit the text, and the syntax highlighting will update immediately.

    TextAreaCustomLanguage 1\u00a0\u00a0class\u00a0HelloWorld\u00a0{\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0publicstatic\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0 4\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 6\u00a0\u00a0

    Recall that we map names (like @heading) from the tree-sitter highlight query to Rich style objects inside the TextAreaTheme.syntax_styles dictionary. If you notice some highlights are missing after registering a language, the issue may be:

    1. The current TextAreaTheme doesn't contain a mapping for the name in the highlight query. Adding a new to syntax_styles should resolve the issue.
    2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name.

    Tip

    The names assigned in tree-sitter highlight queries are often reused across multiple languages. For example, @string is used in many languages to highlight strings.

    "},{"location":"widgets/text_area/#reactive-attributes","title":"Reactive attributes","text":"Name Type Default Description language str | None None The language to use for syntax highlighting. theme str | None TextAreaTheme.default() The theme to use for syntax highlighting. selection Selection Selection() The current selection. show_line_numbers bool True Show or hide line numbers. indent_width int 4 The number of spaces to indent and width of tabs. match_cursor_bracket bool True Enable/disable highlighting matching brackets under cursor. cursor_blink bool True Enable/disable blinking of the cursor when the widget has focus."},{"location":"widgets/text_area/#messages","title":"Messages","text":"
    • TextArea.Changed
    • TextArea.SelectionChanged
    "},{"location":"widgets/text_area/#bindings","title":"Bindings","text":"

    The TextArea widget defines the following bindings:

    Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#component-classes","title":"Component classes","text":"

    The TextArea widget defines no component classes.

    Styling should be done exclusively via TextAreaTheme.

    "},{"location":"widgets/text_area/#see-also","title":"See also","text":"
    • Input - for single-line text input.
    • TextAreaTheme - for theming the TextArea.
    • The tree-sitter documentation website.
    • The tree-sitter Python bindings repository.
    • py-tree-sitter-languages repository (provides binary wheels for a large variety of tree-sitter languages).
    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea","title":"textual.widgets._text_area.TextArea class","text":"
    def __init__(\n    self,\n    text=\"\",\n    *,\n    language=None,\n    theme=None,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView

    Parameters Parameter Default Description text str ''

    The initial text to load into the TextArea.

    language str | None None

    The language to use.

    theme str | None None

    The theme to use.

    name str | None None

    The name of the TextArea widget.

    id str | None None

    The ID of the widget, used to refer to it from Textual CSS.

    classes str | None None

    One or more Textual CSS compatible class names separated by spaces.

    disabled bool False

    True if the widget is disabled.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"escape\",\n        \"screen.focus_next\",\n        \"Shift Focus\",\n        show=False,\n    ),\n    Binding(\"up\", \"cursor_up\", \"cursor up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"cursor down\", show=False\n    ),\n    Binding(\n        \"left\", \"cursor_left\", \"cursor left\", show=False\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"cursor right\", show=False\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_word_left\",\n        \"cursor word left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_word_right\",\n        \"cursor word right\",\n        show=False,\n    ),\n    Binding(\n        \"home,ctrl+a\",\n        \"cursor_line_start\",\n        \"cursor line start\",\n        show=False,\n    ),\n    Binding(\n        \"end,ctrl+e\",\n        \"cursor_line_end\",\n        \"cursor line end\",\n        show=False,\n    ),\n    Binding(\n        \"pageup\",\n        \"cursor_page_up\",\n        \"cursor page up\",\n        show=False,\n    ),\n    Binding(\n        \"pagedown\",\n        \"cursor_page_down\",\n        \"cursor page down\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+shift+left\",\n        \"cursor_word_left(True)\",\n        \"cursor left word select\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+shift+right\",\n        \"cursor_word_right(True)\",\n        \"cursor right word select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+home\",\n        \"cursor_line_start(True)\",\n        \"cursor line start select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+end\",\n        \"cursor_line_end(True)\",\n        \"cursor line end select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+up\",\n        \"cursor_up(True)\",\n        \"cursor up select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+down\",\n        \"cursor_down(True)\",\n        \"cursor down select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+left\",\n        \"cursor_left(True)\",\n        \"cursor left select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+right\",\n        \"cursor_right(True)\",\n        \"cursor right select\",\n        show=False,\n    ),\n    Binding(\"f6\", \"select_line\", \"select line\", show=False),\n    Binding(\"f7\", \"select_all\", \"select all\", show=False),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"delete left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+w\",\n        \"delete_word_left\",\n        \"delete left to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"delete right\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+f\",\n        \"delete_word_right\",\n        \"delete right to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+x\", \"delete_line\", \"delete line\", show=False\n    ),\n    Binding(\n        \"ctrl+u\",\n        \"delete_to_start_of_line\",\n        \"delete to line start\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+k\",\n        \"delete_to_end_of_line\",\n        \"delete to line end\",\n        show=False,\n    ),\n]\n
    Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_languages","title":"available_languages property","text":"
    available_languages: set[str]\n

    A list of the names of languages available to the TextArea.

    The values in this list can be assigned to the language reactive attribute of TextArea.

    The returned list contains the builtin languages plus those registered via the register_language method. Builtin languages will be listed before user-registered languages, but there are no other ordering guarantees.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_themes","title":"available_themes property","text":"
    available_themes: set[str]\n

    A list of the names of the themes available to the TextArea.

    The values in this list can be assigned theme reactive attribute of TextArea.

    You can retrieve the full specification for a theme by passing one of the strings from this list into TextAreaTheme.get_by_name(theme_name: str).

    Alternatively, you can directly retrieve a list of TextAreaTheme objects (which contain the full theme specification) by calling TextAreaTheme.builtin_themes().

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_line","title":"cursor_at_end_of_line property","text":"
    cursor_at_end_of_line: bool\n

    True if and only if the cursor is at the end of a row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_text","title":"cursor_at_end_of_text property","text":"
    cursor_at_end_of_text: bool\n

    True if and only if the cursor is at the very end of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_first_line","title":"cursor_at_first_line property","text":"
    cursor_at_first_line: bool\n

    True if and only if the cursor is on the first line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_last_line","title":"cursor_at_last_line property","text":"
    cursor_at_last_line: bool\n

    True if and only if the cursor is on the last line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_line","title":"cursor_at_start_of_line property","text":"
    cursor_at_start_of_line: bool\n

    True if and only if the cursor is at column 0.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_text","title":"cursor_at_start_of_text property","text":"
    cursor_at_start_of_text: bool\n

    True if and only if the cursor is at location (0, 0)

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_blink","title":"cursor_blink instance-attribute class-attribute","text":"
    cursor_blink: Reactive[bool] = reactive(True)\n

    True if the cursor should blink.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_location","title":"cursor_location property writable","text":"
    cursor_location: Location\n

    The current location of the cursor in the document.

    This is a utility for accessing the end of TextArea.selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset: Offset\n

    The offset of the cursor relative to the screen.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.document","title":"document instance-attribute","text":"
    document: DocumentBase = Document(text)\n

    The document this widget is currently editing.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.gutter_width","title":"gutter_width property","text":"
    gutter_width: int\n

    The width of the gutter (the left column containing line numbers).

    Returns Type Description int

    The cell-width of the line number column. If show_line_numbers is False returns 0.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
    indent_type: Literal['tabs', 'spaces'] = 'spaces'\n

    Whether to indent using tabs or spaces.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_width","title":"indent_width instance-attribute class-attribute","text":"
    indent_width: Reactive[int] = reactive(4)\n

    The width of tabs or the multiple of spaces to align to on pressing the tab key.

    If the document currently open contains tabs that are currently visible on screen, altering this value will immediately change the display width of the visible tabs.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.is_syntax_aware","title":"is_syntax_aware property","text":"
    is_syntax_aware: bool\n

    True if the TextArea is currently syntax aware - i.e. it's parsing document content.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.language","title":"language instance-attribute class-attribute","text":"
    language: Reactive[str | None] = language\n

    The language to use.

    This must be set to a valid, non-None value for syntax highlighting to work.

    If the value is a string, a built-in language parser will be used if available.

    If you wish to use an unsupported language, you'll have to register it first using TextArea.register_language.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.match_cursor_bracket","title":"match_cursor_bracket instance-attribute class-attribute","text":"
    match_cursor_bracket: Reactive[bool] = reactive(True)\n

    If the cursor is at a bracket, highlight the matching bracket (if found).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
    selected_text: str\n

    The text between the start and end points of the current selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selection","title":"selection instance-attribute class-attribute","text":"
    selection: Reactive[Selection] = reactive(\n    Selection(), always_update=True, init=False\n)\n

    The selection start and end locations (zero-based line_index, offset).

    This represents the cursor location and the current selection.

    The Selection.end always refers to the cursor location.

    If no text is selected, then Selection.end == Selection.start is True.

    The text selected in the document is available via the TextArea.selected_text property.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.show_line_numbers","title":"show_line_numbers instance-attribute class-attribute","text":"
    show_line_numbers: Reactive[bool] = reactive(True)\n

    True to show the line number column on the left edge, otherwise False.

    Changing this value will immediately re-render the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text property writable","text":"
    text: str\n

    The entire text content of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.theme","title":"theme instance-attribute class-attribute","text":"
    theme: Reactive[str | None] = theme\n

    The name of the theme to use.

    Themes must be registered using TextArea.register_theme before they can be used.

    Syntax highlighting is only possible when the language attribute is set.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed","title":"Changed class","text":"

    Bases: Message

    Posted when the content inside the TextArea changes.

    Handle this message using the on decorator - @on(TextArea.Changed) or a method named on_text_area_changed.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed.control","title":"control property","text":"
    control: TextArea\n

    The TextArea that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed.text_area","title":"text_area instance-attribute","text":"
    text_area: TextArea\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged","title":"SelectionChanged class","text":"

    Bases: Message

    Posted when the selection changes.

    This includes when the cursor moves or when text is selected.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.selection","title":"selection instance-attribute","text":"
    selection: Selection\n

    The new selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.text_area","title":"text_area instance-attribute","text":"
    text_area: TextArea\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self, select=False):\n

    Move the cursor down one cell.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left method","text":"
    def action_cursor_left(self, select=False):\n

    Move the cursor one location to the left.

    If the cursor is at the left edge of the document, try to move it to the end of the previous line.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end method","text":"
    def action_cursor_line_end(self, select=False):\n

    Move the cursor to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_start","title":"action_cursor_line_start method","text":"
    def action_cursor_line_start(self, select=False):\n

    Move the cursor to the start of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_down","title":"action_cursor_page_down method","text":"
    def action_cursor_page_down(self):\n

    Move the cursor and scroll down one page.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_up","title":"action_cursor_page_up method","text":"
    def action_cursor_page_up(self):\n

    Move the cursor and scroll up one page.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right","title":"action_cursor_right method","text":"
    def action_cursor_right(self, select=False):\n

    Move the cursor one location to the right.

    If the cursor is at the end of a line, attempt to go to the start of the next line.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self, select=False):\n

    Move the cursor up one cell.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left method","text":"
    def action_cursor_word_left(self, select=False):\n

    Move the cursor left by a single word, skipping trailing whitespace.

    Parameters Parameter Default Description select bool False

    Whether to select while moving the cursor.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right method","text":"
    def action_cursor_word_right(self, select=False):\n

    Move the cursor right by a single word, skipping leading whitespace.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_left","title":"action_delete_left method","text":"
    def action_delete_left(self):\n

    Deletes the character to the left of the cursor and updates the cursor location.

    If there's a selection, then the selected range is deleted.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_line","title":"action_delete_line method","text":"
    def action_delete_line(self):\n

    Deletes the lines which intersect with the selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_right","title":"action_delete_right method","text":"
    def action_delete_right(self):\n

    Deletes the character to the right of the cursor and keeps the cursor at the same location.

    If there's a selection, then the selected range is deleted.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line","title":"action_delete_to_end_of_line method","text":"
    def action_delete_to_end_of_line(self):\n

    Deletes from the cursor location to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line method","text":"
    def action_delete_to_start_of_line(self):\n

    Deletes from the cursor location to the start of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_left","title":"action_delete_word_left method","text":"
    def action_delete_word_left(self):\n

    Deletes the word to the left of the cursor and updates the cursor location.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_right","title":"action_delete_word_right method","text":"
    def action_delete_word_right(self):\n

    Deletes the word to the right of the cursor and keeps the cursor at the same location.

    Note that the location that we delete to using this action is not the same as the location we move to when we move the cursor one word to the right. This action does not skip leading whitespace, whereas cursor movement does.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all method","text":"
    def action_select_all(self):\n

    Select all the text in the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line method","text":"
    def action_select_line(self):\n

    Select all the text on the current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index method","text":"
    def cell_width_to_column_index(self, cell_width, row_index):\n

    Return the column that the cell width corresponds to on the given row.

    Parameters Parameter Default Description cell_width int required

    The cell width to convert.

    row_index int required

    The index of the row to examine.

    Returns Type Description int

    The column corresponding to the cell width on that row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable method","text":"
    def clamp_visitable(self, location):\n

    Clamp the given location to the nearest visitable location.

    Parameters Parameter Default Description location Location required

    The location to clamp.

    Returns Type Description Location

    The nearest location that we could conceivably navigate to using the cursor.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear method","text":"
    def clear(self):\n

    Delete all text from the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete method","text":"
    def delete(self, start, end, *, maintain_selection_offset=True):\n

    Delete the text between two locations in the document.

    Parameters Parameter Default Description start Location required

    The start location.

    end Location required

    The end location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit method","text":"
    def edit(self, edit):\n

    Perform an Edit.

    Parameters Parameter Default Description edit Edit required

    The Edit to perform.

    Returns Type Description Any

    Data relating to the edit that may be useful. The data returned

    Any

    may be different depending on the edit performed.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket method","text":"
    def find_matching_bracket(self, bracket, search_from):\n

    If the character is a bracket, find the matching bracket.

    Parameters Parameter Default Description bracket str required

    The character we're searching for the matching bracket of.

    search_from Location required

    The location to start the search.

    Returns Type Description Location | None

    The Location of the matching bracket, or None if it's not found.

    Location | None

    If the character is not available for bracket matching, None is returned.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width method","text":"
    def get_column_width(self, row, column):\n

    Get the cell offset of the column from the start of the row.

    Parameters Parameter Default Description row int required

    The row index.

    column int required

    The column index (codepoint offset from start of row).

    Returns Type Description int

    The cell width of the column relative to the start of the row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location method","text":"
    def get_cursor_down_location(self):\n

    Get the location the cursor will move to if it moves down.

    Returns Type Description Location

    The location the cursor will move to if it moves down.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_left_location","title":"get_cursor_left_location method","text":"
    def get_cursor_left_location(self):\n

    Get the location the cursor will move to if it moves left.

    Returns Type Description Location

    The location of the cursor if it moves left.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_end_location","title":"get_cursor_line_end_location method","text":"
    def get_cursor_line_end_location(self):\n

    Get the location of the end of the current line.

    Returns Type Description Location

    The (row, column) location of the end of the cursors current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_start_location","title":"get_cursor_line_start_location method","text":"
    def get_cursor_line_start_location(self):\n

    Get the location of the start of the current line.

    Returns Type Description Location

    The (row, column) location of the start of the cursors current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location method","text":"
    def get_cursor_right_location(self):\n

    Get the location the cursor will move to if it moves right.

    Returns Type Description Location

    the location the cursor will move to if it moves right.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_up_location","title":"get_cursor_up_location method","text":"
    def get_cursor_up_location(self):\n

    Get the location the cursor will move to if it moves up.

    Returns Type Description Location

    The location the cursor will move to if it moves up.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_left_location","title":"get_cursor_word_left_location method","text":"
    def get_cursor_word_left_location(self):\n

    Get the location the cursor will jump to if it goes 1 word left.

    Returns Type Description Location

    The location the cursor will jump on \"jump word left\".

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_right_location","title":"get_cursor_word_right_location method","text":"
    def get_cursor_word_right_location(self):\n

    Get the location the cursor will jump to if it goes 1 word right.

    Returns Type Description Location

    The location the cursor will jump on \"jump word right\".

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location method","text":"
    def get_target_document_location(self, event):\n

    Given a MouseEvent, return the row and column offset of the event in document-space.

    Parameters Parameter Default Description event MouseEvent required

    The MouseEvent.

    Returns Type Description Location

    The location of the mouse event within the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range method","text":"
    def get_text_range(self, start, end):\n

    Get the text between a start and end location.

    Parameters Parameter Default Description start Location required

    The start location.

    end Location required

    The end location.

    Returns Type Description str

    The text between start and end.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert method","text":"
    def insert(\n    self,\n    text,\n    location=None,\n    *,\n    maintain_selection_offset=True\n):\n

    Insert text into the document.

    Parameters Parameter Default Description text str required

    The text to insert.

    location Location | None None

    The location to insert text, or None to use the cursor location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_document","title":"load_document method","text":"
    def load_document(self, document):\n

    Load a document into the TextArea.

    Parameters Parameter Default Description document DocumentBase required

    The document to load into the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text method","text":"
    def load_text(self, text):\n

    Load text into the TextArea.

    This will replace the text currently in the TextArea.

    Parameters Parameter Default Description text str required

    The text to load into the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor method","text":"
    def move_cursor(\n    self,\n    location,\n    select=False,\n    center=False,\n    record_width=True,\n):\n

    Move the cursor to a location.

    Parameters Parameter Default Description location Location required

    The location to move the cursor to.

    select bool False

    If True, select text between the old and new location.

    center bool False

    If True, scroll such that the cursor is centered.

    record_width bool True

    If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative method","text":"
    def move_cursor_relative(\n    self,\n    rows=0,\n    columns=0,\n    select=False,\n    center=False,\n    record_width=True,\n):\n

    Move the cursor relative to its current location.

    Parameters Parameter Default Description rows int 0

    The number of rows to move down by (negative to move up)

    columns int 0

    The number of columns to move right by (negative to move left)

    select bool False

    If True, select text between the old and new location.

    center bool False

    If True, scroll such that the cursor is centered.

    record_width bool True

    If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width method","text":"
    def record_cursor_width(self):\n

    Record the current cell width of the cursor.

    This is used where we navigate up and down through rows. If we're in the middle of a row, and go down to a row with no content, then we go down to another row, we want our cursor to jump back to the same offset that we were originally at.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language method","text":"
    def register_language(self, language, highlight_query):\n

    Register a language and corresponding highlight query.

    Calling this method does not change the language of the TextArea. On switching to this language (via the language reactive attribute), syntax highlighting will be performed using the given highlight query.

    If a string name is supplied for a builtin supported language, then this method will update the default highlight query for that language.

    Registering a language only registers it to this instance of TextArea.

    Parameters Parameter Default Description language str | 'Language' required

    A string referring to a builtin language or a tree-sitter Language object.

    highlight_query str required

    The highlight query to use for syntax highlighting this language.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme method","text":"
    def register_theme(self, theme):\n

    Register a theme for use by the TextArea.

    After registering a theme, you can set themes by assigning the theme name to the TextArea.theme reactive attribute. For example text_area.theme = \"my_custom_theme\" where \"my_custom_theme\" is the name of the theme you registered.

    If you supply a theme with a name that already exists that theme will be overwritten.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace","title":"replace method","text":"
    def replace(\n    self,\n    insert,\n    start,\n    end,\n    *,\n    maintain_selection_offset=True\n):\n

    Replace text in the document with new text.

    Parameters Parameter Default Description insert str required

    The text to insert.

    start Location required

    The start location

    end Location required

    The end location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible method","text":"
    def scroll_cursor_visible(self, center=False, animate=False):\n

    Scroll the TextArea such that the cursor is visible on screen.

    Parameters Parameter Default Description center bool False

    True if the cursor should be scrolled to the center.

    animate bool False

    True if we should animate while scrolling.

    Returns Type Description Offset

    The offset that was scrolled to bring the cursor into view.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all method","text":"
    def select_all(self):\n

    Select all of the text in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line method","text":"
    def select_line(self, index):\n

    Select all the text in the specified line.

    Parameters Parameter Default Description index int required

    The index of the line to select (starting from 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Highlight","title":"Highlight module-attribute","text":"
    Highlight = Tuple[StartColumn, EndColumn, HighlightName]\n

    A tuple representing a syntax highlight within one line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Location","title":"Location module-attribute","text":"
    Location = Tuple[int, int]\n

    A location (row, column) within the document. Indexing starts at 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document","title":"Document class","text":"
    def __init__(self, text):\n

    Bases: DocumentBase

    A document which can be opened in a TextArea.

    "},{"location":"widgets/text_area/#textual.document._document.Document.line_count","title":"line_count property","text":"
    line_count: int\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.lines","title":"lines property","text":"
    lines: list[str]\n

    Get the document as a list of strings, where each string represents a line.

    Newline characters are not included in at the end of the strings.

    The newline character used in this document can be found via the Document.newline property.

    "},{"location":"widgets/text_area/#textual.document._document.Document.newline","title":"newline property","text":"
    newline: Newline\n

    Get the Newline used in this document (e.g. ' ', ' '. etc.)

    "},{"location":"widgets/text_area/#textual.document._document.Document.text","title":"text property","text":"
    text: str\n

    Get the text from the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_index_from_location","title":"get_index_from_location method","text":"
    def get_index_from_location(self, location):\n

    Given a location, returns the index from the document's text.

    Parameters Parameter Default Description location Location required

    The location in the document.

    Returns Type Description int

    The index in the document's text.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_line","title":"get_line method","text":"
    def get_line(self, index):\n

    Returns the line with the given index from the document.

    Parameters Parameter Default Description index int required

    The index of the line in the document.

    Returns Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_location_from_index","title":"get_location_from_index method","text":"
    def get_location_from_index(self, index):\n

    Given an index in the document's text, returns the corresponding location.

    Parameters Parameter Default Description index int required

    The index in the document's text.

    Returns Type Description Location

    The corresponding location.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_size","title":"get_size method","text":"
    def get_size(self, tab_width):\n

    The Size of the document, taking into account the tab rendering width.

    Parameters Parameter Default Description tab_width int required

    The width to use for tab indents.

    Returns Type Description Size

    The size (width, height) of the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_text_range","title":"get_text_range method","text":"
    def get_text_range(self, start, end):\n

    Get the text that falls between the start and end locations.

    Returns the text between start and end, including the appropriate line separator character as specified by Document._newline. Note that _newline is set automatically to the first line separator character found in the document.

    Parameters Parameter Default Description start Location required

    The start location of the selection.

    end Location required

    The end location of the selection.

    Returns Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.document._document.Document.replace_range","title":"replace_range method","text":"
    def replace_range(self, start, end, text):\n

    Replace text at the given range.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The EditResult containing information about the completed replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase class","text":"

    Bases: ABC

    Describes the minimum functionality a Document implementation must provide in order to be used by the TextArea widget.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.line_count","title":"line_count abstractmethod property","text":"
    line_count: int\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.newline","title":"newline abstractmethod property","text":"
    newline: Newline\n

    Return the line separator used in the document.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.text","title":"text abstractmethod property","text":"
    text: str\n

    The text from the document as a string.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_line","title":"get_line abstractmethod","text":"
    def get_line(self, index):\n

    Returns the line with the given index from the document.

    This is used in rendering lines, and will be called by the TextArea for each line that is rendered.

    Parameters Parameter Default Description index int required

    The index of the line in the document.

    Returns Type Description str

    The str instance representing the line.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_size","title":"get_size abstractmethod","text":"
    def get_size(self, indent_width):\n

    Get the size of the document.

    The height is generally the number of lines, and the width is generally the maximum cell length of all the lines.

    Parameters Parameter Default Description indent_width int required

    The width to use for tab characters.

    Returns Type Description Size

    The Size of the document bounding box.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
    def get_text_range(self, start, end):\n

    Get the text that falls between the start and end locations.

    Parameters Parameter Default Description start Location required

    The start location of the selection.

    end Location required

    The end location of the selection.

    Returns Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.query_syntax_tree","title":"query_syntax_tree method","text":"
    def query_syntax_tree(\n    self, query, start_point=None, end_point=None\n):\n

    Query the tree-sitter syntax tree.

    The default implementation always returns an empty list.

    To support querying in a subclass, this must be implemented.

    Parameters Parameter Default Description query Query required

    The tree-sitter Query to perform.

    start_point tuple[int, int] | None None

    The (row, column byte) to start the query at.

    end_point tuple[int, int] | None None

    The (row, column byte) to end the query at.

    Returns Type Description list[tuple[Node, str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
    def replace_range(self, start, end, text):\n

    Replace the text at the given range.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit class","text":"

    Implements the Undoable protocol to replace text at some range within a document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.from_location","title":"from_location instance-attribute","text":"
    from_location: Location\n

    The start location of the insert.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.maintain_selection_offset","title":"maintain_selection_offset instance-attribute","text":"
    maintain_selection_offset: bool\n

    If True, the selection will maintain its offset to the replacement range.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.text","title":"text instance-attribute","text":"
    text: str\n

    The text to insert. An empty string is equivalent to deletion.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.to_location","title":"to_location instance-attribute","text":"
    to_location: Location\n

    The end location of the insert

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.after","title":"after method","text":"
    def after(self, text_area):\n

    Possibly update the cursor location after the widget has been refreshed.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea this operation was performed on.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.do","title":"do method","text":"
    def do(self, text_area):\n

    Perform the edit operation.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea to perform the edit on.

    Returns Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.undo","title":"undo method","text":"
    def undo(self, text_area):\n

    Undo the edit operation.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea to undo the insert operation on.

    Returns Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult class","text":"

    Contains information about an edit that has occurred.

    "},{"location":"widgets/text_area/#textual.document._document.EditResult.end_location","title":"end_location instance-attribute","text":"
    end_location: Location\n

    The new end Location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.document._document.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
    replaced_text: str\n

    The text that was replaced.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist class","text":"

    Bases: Exception

    Raised when the user tries to use a language which does not exist. This means a language which is not builtin, or has not been registered.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Selection","title":"Selection class","text":"

    Bases: NamedTuple

    A range of characters within a document from a start point to the end point. The location of the cursor is always considered to be the end point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.end","title":"end instance-attribute class-attribute","text":"
    end: Location = (0, 0)\n

    The end location of the selection.

    If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.is_empty","title":"is_empty property","text":"
    is_empty: bool\n

    Return True if the selection has 0 width, i.e. it's just a cursor.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.start","title":"start instance-attribute class-attribute","text":"
    start: Location = (0, 0)\n

    The start location of the selection.

    If you were to click and drag a selection inside a text-editor, this is where you started dragging.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.cursor","title":"cursor classmethod","text":"
    def cursor(cls, location):\n

    Create a Selection with the same start and end point - a \"cursor\".

    Parameters Parameter Default Description location Location required

    The location to create the zero-width Selection.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument class","text":"
    def __init__(self, text, language):\n

    Bases: Document

    A wrapper around a Document which also maintains a tree-sitter syntax tree when the document is edited.

    The primary reason for this split is actually to keep tree-sitter stuff separate, since it isn't supported in Python 3.7. By having the tree-sitter code isolated in this subclass, it makes it easier to conditionally import. However, it does come with other design flaws (e.g. Document is required to have methods which only really make sense on SyntaxAwareDocument).

    If you're reading this and Python 3.7 is no longer supported by Textual, consider merging this subclass into the Document superclass.

    Parameters Parameter Default Description text str required

    The initial text contained in the document.

    language str | Language required

    The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
    language: Language | None = None\n

    The tree-sitter Language or None if tree-sitter is unavailable.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.get_line","title":"get_line method","text":"
    def get_line(self, line_index):\n

    Return the string representing the line, not including new line characters.

    Parameters Parameter Default Description line_index int required

    The index of the line.

    Returns Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.prepare_query","title":"prepare_query method","text":"
    def prepare_query(self, query):\n

    Prepare a tree-sitter tree query.

    Queries should be prepared once, then reused.

    To execute a query, call query_syntax_tree.

    Parameters Parameter Default Description query str required

    The string query to prepare.

    Returns Type Description Query | None

    The prepared query.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree method","text":"
    def query_syntax_tree(\n    self, query, start_point=None, end_point=None\n):\n

    Query the tree-sitter syntax tree.

    The default implementation always returns an empty list.

    To support querying in a subclass, this must be implemented.

    Parameters Parameter Default Description query Query required

    The tree-sitter Query to perform.

    start_point tuple[int, int] | None None

    The (row, column byte) to start the query at.

    end_point tuple[int, int] | None None

    The (row, column byte) to end the query at.

    Returns Type Description list[tuple['Node', str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.replace_range","title":"replace_range method","text":"
    def replace_range(self, start, end, text):\n

    Replace text at the given range.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme class","text":"

    A theme for the TextArea widget.

    Allows theming the general widget (gutter, selections, cursor, and so on) and mapping of tree-sitter tokens to Rich styles.

    For example, consider the following snippet from the markdown.scm highlight query file. We've assigned the heading_content token type to the name heading.

    (heading_content) @heading\n

    Now, we can map this heading name to a Rich style, and it will be styled as such in the TextArea, assuming a parser which returns a heading_content node is used (as will be the case when language=\"markdown\").

    TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})\n

    We can register this theme with our TextArea using the TextArea.register_theme method, and headings in our markdown files will be styled bold cyan.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.base_style","title":"base_style instance-attribute class-attribute","text":"
    base_style: Style | None = None\n

    The background style of the text area. If None the parent style will be used.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style instance-attribute class-attribute","text":"
    bracket_matching_style: Style | None = None\n

    The style to apply to matching brackets. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style instance-attribute class-attribute","text":"
    cursor_line_gutter_style: Style | None = None\n

    The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_style","title":"cursor_line_style instance-attribute class-attribute","text":"
    cursor_line_style: Style | None = None\n

    The style to apply to the line the cursor is on.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_style","title":"cursor_style instance-attribute class-attribute","text":"
    cursor_style: Style | None = None\n

    The style of the cursor. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.gutter_style","title":"gutter_style instance-attribute class-attribute","text":"
    gutter_style: Style | None = None\n

    The style of the gutter. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.name","title":"name instance-attribute","text":"
    name: str\n

    The name of the theme.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.selection_style","title":"selection_style instance-attribute class-attribute","text":"
    selection_style: Style | None = None\n

    The style of the selection. If None a default selection Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.syntax_styles","title":"syntax_styles instance-attribute class-attribute","text":"
    syntax_styles: dict[str, Style] = field(\n    default_factory=dict\n)\n

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
    def builtin_themes(cls):\n

    Get a list of all builtin TextAreaThemes.

    Returns Type Description list[TextAreaTheme]

    A list of all builtin TextAreaThemes.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.default","title":"default classmethod","text":"
    def default(cls):\n

    Get the default syntax theme.

    Returns Type Description TextAreaTheme

    The default TextAreaTheme (probably \"monokai\").

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
    def get_builtin_theme(cls, theme_name):\n

    Get a TextAreaTheme by name.

    Given a theme_name, return the corresponding TextAreaTheme object.

    Parameters Parameter Default Description theme_name str required

    The name of the theme.

    Returns Type Description TextAreaTheme | None

    The TextAreaTheme corresponding to the name or None if the theme isn't found.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_highlight","title":"get_highlight method","text":"
    def get_highlight(self, name):\n

    Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

    Parameters Parameter Default Description name str required

    The name of the highlight.

    Returns Type Description Style | None

    The Style to use for this highlight, or None if no style.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist class","text":"

    Bases: Exception

    Raised when the user tries to use a theme which does not exist. This means a theme which is not builtin, or has not been registered.

    "},{"location":"widgets/toast/","title":"Toast","text":"

    Added in version 0.30.0

    A widget which displays a notification message.

    • Focusable
    • Container

    Note that Toast isn't designed to be used directly in your applications, but it is instead used by notify to display a message when using Textual's built-in notification system.

    "},{"location":"widgets/toast/#styling","title":"Styling","text":"

    You can customize the style of Toasts by targeting the Toast CSS type. For example:

    Toast {\n    padding: 3;\n}\n

    The three severity levels also have corresponding classes, allowing you to target the different styles of notification. They are:

    • -information
    • -warning
    • -error

    If you wish to tailor the notifications for your application you can add rules to your CSS like this:

    Toast.-information {\n    /* Styling here. */\n}\n\nToast.-warning {\n    /* Styling here. */\n}\n\nToast.-error {\n    /* Styling here. */\n}\n

    You can customize just the title wih the toast--title class. The following would make the title italic for an information toast:

    Toast.-information .toast--title {\n    text-style: italic;\n}\n
    "},{"location":"widgets/toast/#example","title":"Example","text":"Outputtoast.py

    ToastApp \u258c\u2590 \u258cIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0\u2590 \u258cchecks\u00a0out.\u2590 \u258c\u2590 \u258c\u2590 \u258cPossible\u00a0trap\u00a0detected\u2590 \u258cNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u2590 \u258cfully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u2590 \u258cbattle\u00a0station!\u2590 \u258c\u2590 \u258c\u2590 \u258cIt's\u00a0a\u00a0trap!\u2590 \u258c\u2590 \u258c\u2590 \u258cIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0\u2590 \u258cimpersonate\u00a0a\u00a0deity.\u2590 \u258c\u2590

    from textual.app import App\n\n\nclass ToastApp(App[None]):\n    def on_mount(self) -> None:\n        # Show an information notification.\n        self.notify(\"It's an older code, sir, but it checks out.\")\n\n        # Show a warning. Note that Textual's notification system allows\n        # for the use of Rich console markup.\n        self.notify(\n            \"Now witness the firepower of this fully \"\n            \"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\n            title=\"Possible trap detected\",\n            severity=\"warning\",\n        )\n\n        # Show an error. Set a longer timeout so it's noticed.\n        self.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n\n        # Show an information notification, but without any sort of title.\n        self.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n\n\nif __name__ == \"__main__\":\n    ToastApp().run()\n
    "},{"location":"widgets/toast/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/toast/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/toast/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/toast/#component-classes","title":"Component Classes","text":"

    The toast widget provides the following component classes:

    Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast.Toast","title":"textual.widgets._toast.Toast class","text":"
    def __init__(self, notification):\n

    Bases: Static

    A widget for displaying short-lived notifications.

    Parameters Parameter Default Description notification Notification required

    The notification to show in the toast.

    "},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {'toast--title'}\n
    Class Description toast--title Targets the title of the toast."},{"location":"widgets/tree/","title":"Tree","text":"

    Added in version 0.6.0

    A tree control widget.

    • Focusable
    • Container
    "},{"location":"widgets/tree/#example","title":"Example","text":"

    The example below creates a simple tree.

    Outputtree.py

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\n\n\nclass TreeApp(App):\n    def compose(self) -> ComposeResult:\n        tree: Tree[dict] = Tree(\"Dune\")\n        tree.root.expand()\n        characters = tree.root.add(\"Characters\", expand=True)\n        characters.add_leaf(\"Paul\")\n        characters.add_leaf(\"Jessica\")\n        characters.add_leaf(\"Chani\")\n        yield tree\n\n\nif __name__ == \"__main__\":\n    app = TreeApp()\n    app.run()\n

    Tree widgets have a \"root\" attribute which is an instance of a TreeNode. Call add() or add_leaf() to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.

    "},{"location":"widgets/tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/tree/#messages","title":"Messages","text":"
    • Tree.NodeCollapsed
    • Tree.NodeExpanded
    • Tree.NodeHighlighted
    • Tree.NodeSelected
    "},{"location":"widgets/tree/#bindings","title":"Bindings","text":"

    The tree widget defines the following bindings:

    Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#component-classes","title":"Component Classes","text":"

    The tree widget provides the following component classes:

    Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items.

    Make non-widget Tree support classes available.

    "},{"location":"widgets/tree/#textual.widgets.Tree","title":"textual.widgets.Tree class","text":"
    def __init__(\n    self,\n    label,\n    data=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[TreeDataType], ScrollView

    A widget for displaying and navigating data in a tree.

    Parameters Parameter Default Description label TextType required

    The label of the root node of the tree.

    data TreeDataType | None None

    The optional data to associate with the root node of the tree.

    name str | None None

    The name of the Tree.

    id str | None None

    The ID of the tree in the DOM.

    classes str | None None

    The CSS classes of the tree.

    disabled bool False

    Whether the tree is disabled or not.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"space\", \"toggle_node\", \"Toggle\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#textual.widgets._tree.Tree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"tree--cursor\",\n    \"tree--guides\",\n    \"tree--guides-hover\",\n    \"tree--guides-selected\",\n    \"tree--highlight\",\n    \"tree--highlight-line\",\n    \"tree--label\",\n}\n
    Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets._tree.Tree.auto_expand","title":"auto_expand instance-attribute class-attribute","text":"
    auto_expand = var(True)\n

    Auto expand tree nodes when clicked.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_line","title":"cursor_line instance-attribute class-attribute","text":"
    cursor_line = var(-1, always_update=True)\n

    The line with the cursor, or -1 if no cursor.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_node","title":"cursor_node property","text":"
    cursor_node: TreeNode[TreeDataType] | None\n

    The currently selected node, or None if no selection.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.guide_depth","title":"guide_depth instance-attribute class-attribute","text":"
    guide_depth = reactive(4, init=False)\n

    The indent depth of tree nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.hover_line","title":"hover_line instance-attribute class-attribute","text":"
    hover_line = var(-1)\n

    The line number under the mouse pointer, or -1 if not under the mouse pointer.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.last_line","title":"last_line property","text":"
    last_line: int\n

    The index of the last line.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.root","title":"root instance-attribute","text":"
    root = self._add_node(None, text_label, data)\n

    The root node of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_guides","title":"show_guides instance-attribute class-attribute","text":"
    show_guides = reactive(True)\n

    Enable display of tree guide lines.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_root","title":"show_root instance-attribute class-attribute","text":"
    show_root = reactive(True)\n

    Show the root of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed","title":"NodeCollapsed class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is collapsed.

    Can be handled using on_tree_node_collapsed in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was collapsed.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded","title":"NodeExpanded class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is expanded.

    Can be handled using on_tree_node_expanded in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was expanded.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted","title":"NodeHighlighted class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is highlighted.

    Can be handled using on_tree_node_highlighted in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was highlighted.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected","title":"NodeSelected class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is selected.

    Can be handled using on_tree_node_selected in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was selected.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Move the cursor down one node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Move the cursor up one node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the cursor down a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the cursor up a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_end","title":"action_scroll_end method","text":"
    def action_scroll_end(self):\n

    Move the cursor to the bottom of the tree.

    Note

    Here bottom means vertically, not branch depth.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_home","title":"action_scroll_home method","text":"
    def action_scroll_home(self):\n

    Move the cursor to the top of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_select_cursor","title":"action_select_cursor method","text":"
    def action_select_cursor(self):\n

    Cause a select event for the target node.

    Note

    If auto_expand is True use of this action on a non-leaf node will cause both an expand/collapse event to occur, as well as a selected event.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_toggle_node","title":"action_toggle_node method","text":"
    def action_toggle_node(self):\n

    Toggle the expanded state of the target node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all nodes under root.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_label_width","title":"get_label_width method","text":"
    def get_label_width(self, node):\n

    Get the width of the nodes label.

    The default behavior is to call render_node and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    A node.

    Returns Type Description int

    Width in cells.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_at_line","title":"get_node_at_line method","text":"
    def get_node_at_line(self, line_no):\n

    Get the node for a given line.

    Parameters Parameter Default Description line_no int required

    A line number.

    Returns Type Description TreeNode[TreeDataType] | None

    A tree node, or None if there is no node at that line.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_by_id","title":"get_node_by_id method","text":"
    def get_node_by_id(self, node_id):\n

    Get a tree node by its ID.

    Parameters Parameter Default Description node_id NodeID required

    The ID of the node to get.

    Returns Type Description TreeNode[TreeDataType]

    The node associated with that ID.

    Raises Type Description UnknownNodeID

    Raised if the TreeNode ID is unknown.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.process_label","title":"process_label method","text":"
    def process_label(self, label):\n

    Process a str or Text value into a label.

    Maybe overridden in a subclass to change how labels are rendered.

    Parameters Parameter Default Description label TextType required

    Label.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.render_label","title":"render_label method","text":"
    def render_label(self, node, base_style, style):\n

    Render a label for the given node. Override this to modify how labels are rendered.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    A tree node.

    base_style Style required

    The base style of the widget.

    style Style required

    The additional style for the label.

    Returns Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.reset","title":"reset method","text":"
    def reset(self, label, data=None):\n

    Clear the tree and reset the root node.

    Parameters Parameter Default Description label TextType required

    The label for the root node.

    data TreeDataType | None None

    Optional data for the root node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_line","title":"scroll_to_line method","text":"
    def scroll_to_line(self, line, animate=True):\n

    Scroll to the given line.

    Parameters Parameter Default Description line int required

    A line number.

    animate bool True

    Enable animation.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_node","title":"scroll_to_node method","text":"
    def scroll_to_node(self, node, animate=True):\n

    Scroll to the given node.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    Node to scroll in to view.

    animate bool True

    Animate scrolling.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.select_node","title":"select_node method","text":"
    def select_node(self, node):\n

    Move the cursor to the given node, or reset cursor.

    Parameters Parameter Default Description node TreeNode[TreeDataType] | None required

    A tree node, or None to reset cursor.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_cursor_line","title":"validate_cursor_line method","text":"
    def validate_cursor_line(self, value):\n

    Prevent cursor line from going outside of range.

    Parameters Parameter Default Description value int required

    The value to test.

    Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_guide_depth","title":"validate_guide_depth method","text":"
    def validate_guide_depth(self, value):\n

    Restrict guide depth to reasonable range.

    Parameters Parameter Default Description value int required

    The value to test.

    Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.tree.EventTreeDataType","title":"EventTreeDataType module-attribute","text":"
    EventTreeDataType = TypeVar('EventTreeDataType')\n

    The type of the data for a given instance of a Tree.

    Similar to TreeDataType but used for Tree messages.

    "},{"location":"widgets/tree/#textual.widgets.tree.NodeID","title":"NodeID module-attribute","text":"
    NodeID = NewType('NodeID', int)\n

    The type of an ID applied to a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeDataType","title":"TreeDataType module-attribute","text":"
    TreeDataType = TypeVar('TreeDataType')\n

    The type of the data for a given instance of a Tree.

    "},{"location":"widgets/tree/#textual.widgets.tree.RemoveRootError","title":"RemoveRootError class","text":"

    Bases: Exception

    Exception raised when trying to remove the root of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"TreeNode class","text":"
    def __init__(\n    self,\n    tree,\n    parent,\n    id,\n    label,\n    data=None,\n    *,\n    expanded=True,\n    allow_expand=True\n):\n

    Bases: Generic[TreeDataType]

    An object that represents a \"node\" in a tree control.

    Parameters Parameter Default Description tree Tree[TreeDataType] required

    The tree that the node is being attached to.

    parent TreeNode[TreeDataType] | None required

    The parent node that this node is being attached to.

    id NodeID required

    The ID of the node.

    label Text required

    The label for the node.

    data TreeDataType | None None

    Optional data to associate with the node.

    expanded bool True

    Should the node be attached in an expanded state?

    allow_expand bool True

    Should the node allow being expanded by the user?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.allow_expand","title":"allow_expand property writable","text":"
    allow_expand: bool\n

    Is this node allowed to expand?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.children","title":"children property","text":"
    children: TreeNodes[TreeDataType]\n

    The child nodes of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.data","title":"data instance-attribute","text":"
    data = data\n

    Optional data associated with the tree node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.id","title":"id property","text":"
    id: NodeID\n

    The ID of the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_expanded","title":"is_expanded property","text":"
    is_expanded: bool\n

    Is the node expanded?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_last","title":"is_last property","text":"
    is_last: bool\n

    Is this the last child node of its parent?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_root","title":"is_root property","text":"
    is_root: bool\n

    Is this node the root of the tree?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.label","title":"label property writable","text":"
    label: TextType\n

    The label for the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.line","title":"line property","text":"
    line: int\n

    The line number for this node, or -1 if it is not displayed.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.parent","title":"parent property","text":"
    parent: TreeNode[TreeDataType] | None\n

    The parent of the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.tree","title":"tree property","text":"
    tree: Tree[TreeDataType]\n

    The tree that this node is attached to.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add","title":"add method","text":"
    def add(\n    self,\n    label,\n    data=None,\n    *,\n    expand=False,\n    allow_expand=True\n):\n

    Add a node to the sub-tree.

    Parameters Parameter Default Description label TextType required

    The new node's label.

    data TreeDataType | None None

    Data associated with the new node.

    expand bool False

    Node should be expanded.

    allow_expand bool True

    Allow use to expand the node via keyboard or mouse.

    Returns Type Description TreeNode[TreeDataType]

    A new Tree node

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add_leaf","title":"add_leaf method","text":"
    def add_leaf(self, label, data=None):\n

    Add a 'leaf' node (a node that can not expand).

    Parameters Parameter Default Description label TextType required

    Label for the node.

    data TreeDataType | None None

    Optional data.

    Returns Type Description TreeNode[TreeDataType]

    New node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse","title":"collapse method","text":"
    def collapse(self):\n

    Collapse the node (hide its children).

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse_all","title":"collapse_all method","text":"
    def collapse_all(self):\n

    Collapse the node (hide its children) and all those below it.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand","title":"expand method","text":"
    def expand(self):\n

    Expand the node (show its children).

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand_all","title":"expand_all method","text":"
    def expand_all(self):\n

    Expand the node (show its children) and all those below it.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.refresh","title":"refresh method","text":"
    def refresh(self):\n

    Initiate a refresh (repaint) of this node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove","title":"remove method","text":"
    def remove(self):\n

    Remove this node from the tree.

    Raises Type Description RemoveRootError

    If there is an attempt to remove the root.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove_children","title":"remove_children method","text":"
    def remove_children(self):\n

    Remove any child nodes of this node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.set_label","title":"set_label method","text":"
    def set_label(self, label):\n

    Set a new label for the node.

    Parameters Parameter Default Description label TextType required

    A str or Text object with the new label.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle","title":"toggle method","text":"
    def toggle(self):\n

    Toggle the node's expanded state.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle_all","title":"toggle_all method","text":"
    def toggle_all(self):\n

    Toggle the node's expanded state and make all those below it match.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets.tree.UnknownNodeID","title":"UnknownNodeID class","text":"

    Bases: Exception

    Exception raised when referring to an unknown TreeNode ID.

    "},{"location":"blog/archive/2023/","title":"2023","text":""},{"location":"blog/archive/2022/","title":"2022","text":""},{"location":"blog/category/devlog/","title":"DevLog","text":""},{"location":"blog/category/release/","title":"Release","text":""},{"location":"blog/category/news/","title":"News","text":""},{"location":"blog/page/2/","title":"Textual Blog","text":""},{"location":"blog/page/3/","title":"Textual Blog","text":""},{"location":"blog/page/4/","title":"Textual Blog","text":""},{"location":"blog/archive/2023/page/2/","title":"2023","text":""},{"location":"blog/archive/2023/page/3/","title":"2023","text":""},{"location":"blog/archive/2022/page/2/","title":"2022","text":""},{"location":"blog/category/devlog/page/2/","title":"DevLog","text":""},{"location":"blog/category/release/page/2/","title":"Release","text":""}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

    Tip

    See the navigation links in the header or side-bar.

    Click (top left) on mobile.

    "},{"location":"#welcome","title":"Welcome","text":"

    Welcome to the Textual framework documentation.

    Get started or go straight to the Tutorial

    "},{"location":"#what-is-textual","title":"What is Textual?","text":"

    Textual is a Rapid Application Development framework for Python, built by Textualize.io.

    Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal or a web browser!

    • Rapid development

      Uses your existing Python skills to build beautiful user interfaces.

    • Low requirements

      Run Textual on a single board computer if you want to.

    • Cross platform

      Textual runs just about everywhere.

    • Remote

      Textual apps can run over SSH.

    • CLI Integration

      Textual apps can be launched and run from the command prompt.

    • Open Source

      Textual is licensed under MIT.

    CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 C+/-%\u00f7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 789\u00d7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 456- \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 123+ \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 0.= \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    PrideApp

    StopwatchApp \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:04.04 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2586\u2586\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a05\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a06\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"FAQ/","title":"FAQ","text":""},{"location":"FAQ/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

    Welcome to the Textual FAQ. Here we try and answer any question that comes up frequently. If you can't find what you are looking for here, see our other help channels.

    "},{"location":"FAQ/#does-textual-support-images","title":"Does Textual support images?","text":"

    Textual doesn't have built-in support for images yet, but it is on the Roadmap.

    See also the rich-pixels project for a Rich renderable for images that works with Textual.

    "},{"location":"FAQ/#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp","title":"How can I fix ImportError cannot import name ComposeResult from textual.app ?","text":"

    You likely have an older version of Textual. You can install the latest version by adding the -U switch which will force pip to upgrade.

    The following should do it:

    pip install textual-dev -U\n

    "},{"location":"FAQ/#how-can-i-select-and-copy-text-in-a-textual-app","title":"How can I select and copy text in a Textual app?","text":"

    Running a Textual app puts your terminal in to application mode which disables clicking and dragging to select text. Most terminal emulators offer a modifier key which you can hold while you click and drag to restore the behavior you may expect from the command line. The exact modifier key depends on the terminal and platform you are running on.

    • iTerm Hold the OPTION key.
    • Gnome Terminal Hold the SHIFT key.
    • Windows Terminal Hold the SHIFT key.

    Refer to the documentation for your terminal emulator, if it is not listed above.

    "},{"location":"FAQ/#how-can-i-set-a-translucent-app-background","title":"How can I set a translucent app background?","text":"

    Some terminal emulators have a translucent background feature which allows the desktop underneath to be partially visible.

    This feature is unlikely to work with Textual, as the translucency effect requires the use of ANSI background colors, which Textual doesn't use. Textual uses 16.7 million colors where available which enables consistent colors across all platforms and additional effects which aren't possible with ANSI colors.

    For more information on ANSI colors in Textual, see Why no Ansi Themes?.

    "},{"location":"FAQ/#how-do-i-center-a-widget-in-a-screen","title":"How do I center a widget in a screen?","text":"

    Tip

    See How To Center Things in the Textual documentation for a more comprensive answer to this question.

    To center a widget within a container use align. But remember that align works on the children of a container, it isn't something you use on the child you want centered.

    For example, here's an app that shows a Button in the middle of a Screen:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nclass ButtonApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"PUSH ME!\")\n\nif __name__ == \"__main__\":\n    ButtonApp().run()\n

    If you use the above on multiple widgets, you'll find they appear to \"left-align\" in the center of the screen, like this:

    +-----+\n|     |\n+-----+\n\n+---------+\n|         |\n+---------+\n\n+---------------+\n|               |\n+---------------+\n

    If you want them more like this:

         +-----+\n     |     |\n     +-----+\n\n   +---------+\n   |         |\n   +---------+\n\n+---------------+\n|               |\n+---------------+\n

    the best approach is to wrap each widget in a Center container that individually centers it. For example:

    from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Button\n\nclass ButtonApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Center(Button(\"PUSH ME!\"))\n        yield Center(Button(\"AND ME!\"))\n        yield Center(Button(\"ALSO PLEASE PUSH ME!\"))\n        yield Center(Button(\"HEY ME ALSO!!\"))\n\nif __name__ == \"__main__\":\n    ButtonApp().run()\n

    "},{"location":"FAQ/#how-do-i-fix-workerdeclarationerror","title":"How do I fix WorkerDeclarationError?","text":"

    Textual version 0.31.0 requires that you set thread=True on the @work decorator if you want to run a threaded worker.

    If you want a threaded worker, you would declare it in the following way:

    @work(thread=True)\ndef run_in_background():\n    ...\n

    If you don't want a threaded worker, you should make your work function async:

    @work()\nasync def run_in_background():\n    ...\n

    This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results.

    "},{"location":"FAQ/#how-do-i-pass-arguments-to-an-app","title":"How do I pass arguments to an app?","text":"

    When creating your App class, override __init__ as you would when inheriting normally. For example:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nclass Greetings(App[None]):\n\n    def __init__(self, greeting: str=\"Hello\", to_greet: str=\"World\") -> None:\n        self.greeting = greeting\n        self.to_greet = to_greet\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Static(f\"{self.greeting}, {self.to_greet}\")\n

    Then the app can be run, passing in various arguments; for example:

    # Running with default arguments.\nGreetings().run()\n\n# Running with a keyword argument.\nGreetings(to_greet=\"davep\").run()\n\n# Running with both positional arguments.\nGreetings(\"Well hello\", \"there\").run()\n

    "},{"location":"FAQ/#no-widget-called-textlog","title":"No widget called TextLog","text":"

    The TextLog widget was renamed to RichLog in Textual 0.32.0. You will need to replace all references to TextLog in your code, with RichLog. Most IDEs will have a search and replace function which will help you do this.

    Here's how you should import RichLog:

    from textual.widgets import RichLog\n

    "},{"location":"FAQ/#why-do-some-key-combinations-never-make-it-to-my-app","title":"Why do some key combinations never make it to my app?","text":"

    Textual can only ever support key combinations that are passed on by your terminal application. Which keys get passed on can differ from terminal to terminal, and from operating system to operating system.

    Because of this it's best to stick to key combinations that are known to be universally-supported; these include the likes of:

    • Letters
    • Numbers
    • Numbered function keys (especially F1 through F10)
    • Space
    • Return
    • Arrow, home, end and page keys
    • Control
    • Shift

    When creating bindings for your application we recommend picking keys and key combinations from the above.

    Keys that aren't normally passed through by terminals include Cmd and Option on macOS, and the Windows key on Windows.

    If you need to test what key combinations work in different environments you can try them out with textual keys.

    "},{"location":"FAQ/#why-doesnt-textual-look-good-on-macos","title":"Why doesn't Textual look good on macOS?","text":"

    You may find that the default macOS Terminal.app doesn't render Textual apps (and likely other TUIs) very well, particuarily when it comes to box characters. For instance, you may find it displays misaligned blocks and lines like this:

    You can (mostly) fix this by opening settings -> profiles > Text tab, and changing the font settings. We have found that Menlo Regular font, with a character spacing of 1 and line spacing of 0.805 produces reasonable results. If you want to use another font, you may have to tweak the line spacing until you get good results.

    With these changes, Textual apps render more as intended:

    Even with this fix, Terminal.app has a few limitations. It is limited to 256 colors, and can be a little slow compared to more modern alternatives. Fortunately there are a number of free terminal emulators for macOS which produces high quality results.

    We recommend any of the following terminals:

    • iTerm2
    • Kitty
    • WezTerm
    "},{"location":"FAQ/#terminalapp-colors","title":"Terminal.app colors","text":""},{"location":"FAQ/#iterm2-colors","title":"iTerm2 colors","text":""},{"location":"FAQ/#why-doesnt-textual-support-ansi-themes","title":"Why doesn't Textual support ANSI themes?","text":"

    Textual will not generate escape sequences for the 16 themeable ANSI colors.

    This is an intentional design decision we took for for the following reasons:

    • Not everyone has a carefully chosen ANSI color theme. Color combinations which may look fine on your system, may be unreadable on another machine. There is very little an app author or Textual can do to resolve this. Asking users to simply pick a better theme is not a good solution, since not all users will know how.
    • ANSI colors can't be manipulated in the way Textual can do with other colors. Textual can blend colors and produce light and dark shades from an original color, which is used to create more readable text and user interfaces. Color blending will also be used to power future accessibility features.

    Textual has a design system which guarantees apps will be readable on all platforms and terminals, and produces better results than ANSI colors.

    There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme.

    "},{"location":"FAQ/#why-doesnt-the-datatable-scroll-programmatically","title":"Why doesn't the DataTable scroll programmatically?","text":"

    If scrolling in your DataTable is apparently broken, it may be because your DataTable is using the default value of height: auto. This means that the table will be sized to fit its rows without scrolling, which may cause the container (typically the screen) to scroll. If you would like the table itself to scroll, set the height to something other than auto, like 100%.

    NOTE: As of Textual v0.31.0 the max-height of a DataTable is set to 100%, this will mean that the above is no longer the default experience.

    Generated by FAQtory

    "},{"location":"getting_started/","title":"Getting started","text":"

    All you need to get started building Textual apps.

    "},{"location":"getting_started/#requirements","title":"Requirements","text":"

    Textual requires Python 3.8 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.

    Your platform

    "},{"location":"getting_started/#linux-all-distros","title":"Linux (all distros)","text":"

    All Linux distros come with a terminal emulator that can run Textual apps.

    "},{"location":"getting_started/#macos","title":"macOS","text":"

    The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as iterm2, Kitty, or WezTerm.

    "},{"location":"getting_started/#windows","title":"Windows","text":"

    The new Windows Terminal runs Textual apps beautifully.

    "},{"location":"getting_started/#installation","title":"Installation","text":"

    Here's how to install Textual.

    "},{"location":"getting_started/#from-pypi","title":"From PyPI","text":"

    You can install Textual via PyPI, with the following command:

    pip install textual\n

    If you plan on developing Textual apps, you should also install textual developer tools:

    pip install textual-dev\n
    "},{"location":"getting_started/#from-conda-forge","title":"From conda-forge","text":"

    Textual is also available on conda-forge. The preferred package manager for conda-forge is currently micromamba:

    micromamba install -c conda-forge textual\n

    And for the textual developer tools:

    micromamba install -c conda-forge textual-dev\n
    "},{"location":"getting_started/#textual-cli","title":"Textual CLI","text":"

    If you installed the developer tools you should have access to the textual command. There are a number of sub-commands available which will aid you in building Textual apps. Run the following for a list of the available commands:

    textual --help\n

    See devtools for more about the textual command.

    "},{"location":"getting_started/#demo","title":"Demo","text":"

    Once you have Textual installed, run the following to get an impression of what it can do:

    python -m textual\n

    If Textual is installed you should see the following:

    Textual\u00a0Demo \u2b58Textual\u00a0Demo TOP\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Widgets\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Textual\u00a0widgets\u00a0are\u00a0powerful\u00a0interactive\u00a0components.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Widgets Build\u00a0your\u00a0own\u00a0or\u00a0use\u00a0the\u00a0builtin\u00a0widgets.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Input\u00a0Text\u00a0/\u00a0Password\u00a0input.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Rich\u00a0content\u00a0\u2022\u00a0Button\u00a0Clickable\u00a0button\u00a0with\u00a0a\u00a0number\u00a0of\u00a0styles.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Switch\u00a0A\u00a0switch\u00a0to\u00a0toggle\u00a0between\u00a0states.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2583\u2583 \u00a0\u2022\u00a0DataTable\u00a0A\u00a0spreadsheet-like\u00a0widget\u00a0for\u00a0navigating\u00a0data.\u00a0Cells\u00a0may\u00a0contain\u00a0text\u00a0or\u00a0Rich\u00a0 renderables.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 CSS\u00a0\u2022\u00a0Tree\u00a0An\u00a0generic\u00a0tree\u00a0with\u00a0expandable\u00a0nodes.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0DirectoryTree\u00a0A\u00a0tree\u00a0of\u00a0file\u00a0and\u00a0folders.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0...\u00a0many\u00a0more\u00a0planned\u00a0... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258eUsername\u258awill\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a\u2585\u2585 \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258ePassword\u258aPassword\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a \u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258a \u258eLogin\u258a \u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bar\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Baz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(0,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(1,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(2,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(3,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(4,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(5,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(6,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(7,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(8,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(9,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582 \u00a0Cell\u00a0(10,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(11,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(12,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(13,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258f \u00a0CTRL+B\u00a0\u00a0Sidebar\u00a0\u00a0CTRL+T\u00a0\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0CTRL+S\u00a0\u00a0Screenshot\u00a0\u00a0F1\u00a0\u00a0Notes\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    "},{"location":"getting_started/#examples","title":"Examples","text":"

    The Textual repository comes with a number of example apps. To try out the examples, first clone the Textual repository:

    HTTPSSSHGitHub CLI
    git clone https://github.com/Textualize/textual.git\n
    git clone git@github.com:Textualize/textual.git\n
    gh repo clone Textualize/textual\n

    With the repository cloned, navigate to the /examples/ directory where you will find a number of Python files you can run from the command line:

    cd textual/examples/\npython code_browser.py ../\n
    "},{"location":"getting_started/#need-help","title":"Need help?","text":"

    See the help page for how to get help with Textual, or to report bugs.

    "},{"location":"help/","title":"Help","text":"

    If you need help with any aspect of Textual, let us know! We would be happy to hear from you.

    "},{"location":"help/#bugs-and-feature-requests","title":"Bugs and feature requests","text":"

    Report bugs via GitHub on the Textual issues page. You can also post feature requests via GitHub issues, but see the roadmap first.

    "},{"location":"help/#help-with-using-textual","title":"Help with using Textual","text":"

    You can seek help with using Textual in the discussion area on GitHub.

    "},{"location":"help/#discord-server","title":"Discord Server","text":"

    For more realtime feedback or chat, join our discord server to connect with the Textual community.

    "},{"location":"roadmap/","title":"Roadmap","text":"

    We (textualize.io) are actively building and maintaining Textual.

    We have many new features in the pipeline. This page will keep track of that work.

    "},{"location":"roadmap/#features","title":"Features","text":"

    High-level features we plan on implementing.

    • Accessibility
      • Integration with screen readers
      • Monochrome mode
      • High contrast theme
      • Color-blind themes
    • Command palette
      • Fuzzy search
    • Configuration (.toml based extensible configuration format)
    • Console
    • Devtools
      • Integrated log
      • DOM tree view
      • REPL
    • Reactive state abstraction
    • Themes
      • Customize via config
      • Builtin theme editor
    "},{"location":"roadmap/#widgets","title":"Widgets","text":"

    Widgets are key to making user-friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.

    • Buttons
      • Error / warning variants
    • Color picker
    • Checkbox
    • Content switcher
    • DataTable
      • Cell select
      • Row / Column select
      • API to update cells / rows
      • Lazy loading API
    • Date picker
    • Drop-down menus
    • Form Widget
      • Serialization / Deserialization
      • Export to attrs objects
      • Export to PyDantic objects
    • Image support
      • Half block
      • Braille
      • Sixels, and other image extensions
    • Input
      • Validation
      • Error / warning states
      • Template types: IP address, physical units (weight, volume), currency, credit card etc
    • Select control (pull-down)
    • Markdown viewer
      • Collapsible sections
      • Custom widgets
    • Plots
      • bar chart
      • line chart
      • Candlestick chars
    • Progress bars
      • Style variants (solid, thin etc)
    • Radio boxes
    • Spark-lines
    • Switch
    • Tabs
    • TextArea (multi-line input)
      • Basic controls
      • Indentation guides
      • Smart features for various languages
      • Syntax highlighting
    "},{"location":"tutorial/","title":"Tutorial","text":"

    Welcome to the Textual Tutorial!

    By the end of this page you should have a solid understanding of app development with Textual.

    Quote

    If you want people to build things, make it fun.

    \u2014 Will McGugan (creator of Rich and Textual)

    "},{"location":"tutorial/#video-series","title":"Video series","text":"

    This tutorial has an accompanying video series which covers the same content.

    "},{"location":"tutorial/#stopwatch-application","title":"Stopwatch Application","text":"

    We're going to build a stopwatch application. This application should show a list of stopwatches with buttons to start, stop, and reset the stopwatches. We also want the user to be able to add and remove stopwatches as required.

    This will be a simple yet fully featured app \u2014 you could distribute this app if you wanted to!

    Here's what the finished app will look like:

    stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:15.24 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:12.12 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:08.09 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    "},{"location":"tutorial/#try-it-out","title":"Try it out!","text":"

    The following is not a screenshot, but a fully interactive Textual app running in your browser.

    Try in Textual-web

    Tip

    See textual-web if you are interested in publishing your Textual apps on the web.

    "},{"location":"tutorial/#get-the-code","title":"Get the code","text":"

    If you want to try the finished Stopwatch app and follow along with the code, first make sure you have Textual installed then check out the Textual repository:

    HTTPSSSHGitHub CLI
    git clone https://github.com/Textualize/textual.git\n
    git clone git@github.com:Textualize/textual.git\n
    gh repo clone Textualize/textual\n

    With the repository cloned, navigate to docs/examples/tutorial and run stopwatch.py.

    cd textual/docs/examples/tutorial\npython stopwatch.py\n
    "},{"location":"tutorial/#type-hints-in-brief","title":"Type hints (in brief)","text":"

    Tip

    Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.

    We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like mypy to catch bugs before your code runs.

    The following function contains type hints:

    def repeat(text: str, count: int) -> str:\n    \"\"\"Repeat a string a given number of times.\"\"\"\n    return text * count\n

    Parameter types follow a colon. So text: str indicates that text requires a string and count: int means that count requires an integer.

    Return types follow ->. So -> str: indicates this method returns a string.

    "},{"location":"tutorial/#the-app-class","title":"The App class","text":"

    The first step in building a Textual app is to import and extend the App class. Here's a basic app class we will use as a starting point for the stopwatch app.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    If you run this code, you should see something like the following:

    stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Hit the D key to toggle between light and dark mode.

    stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Hit Ctrl+C to exit the app and return to the command prompt.

    "},{"location":"tutorial/#a-closer-look-at-the-app-class","title":"A closer look at the App class","text":"

    Let's examine stopwatch01.py in more detail.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The first line imports the Textual App class, which we will use as the base class for our App. The second line imports two builtin widgets: Footer which shows a bar at the bottom of the screen with bound keys, and Header which shows a title at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.

    The following lines define the app itself:

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more.

    Here's what the above app defines:

    • BINDINGS is a list of tuples that maps (or binds) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the D key on to the \"toggle_dark\" action. See key bindings in the guide for details.

    • compose() is where we construct a user interface with widgets. The compose() method may return a list of widgets, but it is generally easier to yield them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. Header() and Footer().

    • action_toggle_dark() defines an action method. Actions are methods beginning with action_ followed by the name of the action. The BINDINGS list above tells Textual to run this action when the user hits the D key. See actions in the guide for details.

    stopwatch01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The final three lines create an instance of the app and calls the run() method which puts your terminal in to application mode and runs the app until you exit with Ctrl+C. This happens within a __name__ == \"__main__\" block so we could run the app with python stopwatch01.py or import it as part of a larger project.

    "},{"location":"tutorial/#designing-a-ui-with-widgets","title":"Designing a UI with widgets","text":"

    Textual comes with a number of builtin widgets, like Header and Footer, which are versatile and re-usable. We will need to build some custom widgets for the stopwatch. Before we dive in to that, let's first sketch a design for the app \u2014 so we know what we're aiming for.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVcXGlP40pcdTAwMTb93r9cdTAwMDIxX2akxq/2paXRqFx1MDAwM4R9XHUwMDBiW8PMXHUwMDEzMrFDXGbxguNA4Kn/+5SddOwktuM4S/tFLVx1MDAxYVxcjuu66tx77lJVf33Z2NhcZj48c/PbxqbZb+pcdTAwMWTL8PX3za/h9TfT71quo5pQ9HfX7fnN6M52XHUwMDEweN1vf/xh6/6LXHUwMDE5eFx1MDAxZL1pam9Wt6d3ukHPsFxcrenaf1iBaXf/XHUwMDEz/jzVbfPfnmtcdTAwMWKBr8WdbJmGXHUwMDE1uP6gL7Nj2qZcdTAwMTN01dP/q/7e2Pgr+pmQzjebge48dczoXHUwMDBiUVMsIFx1MDAwNmzy6qnrRMJcbi5cdTAwMDWFXGaR0VxyVndHdVx1MDAxN5iGam0pkc24Jby0+aizxo/G/VPr6uX83Hu/ueqSPo97bVmdzmXw0Ymk6rrqZeK2buC7L+atZVx1MDAwNG3VXG4nrmd9y3d7T23H7HbHvuN6etNcbj7Ca1x1MDAwMIyuXHUwMDBlhuDbRnylXHUwMDFmTlx1MDAxMCdcdTAwMWFkXHUwMDAwUoboqCH8KpJcXCOEYpi4Plx1MDAxMGbb7ajBV8L8XHUwMDAzRJ9YnEe9+fKkZHKM+Fx1MDAxZVx1MDAwMpqAJlx1MDAwNuF9+IqqQ1xyy4lcdTAwMGXapvXUXHUwMDBlXHUwMDA2gmtcdTAwMWNcbp7o24yGXHUwMDFkXCLEOUFcYqNRS9ijd2BEXGL4c3Lg2rrvXHJcdTAwMDdos1x1MDAxYv6RkDZcdTAwMTR0d1x1MDAxMj5JXGIlpvag2/C2m5+HRzdcdTAwMTf1t7uL8114vnc4etZcdTAwMTjedN933zdHLT+Hv8Wi9TxDXHUwMDFmgFxiMlx1MDAwNiRcdTAwMTaSXHUwMDEwgmNcdTAwMWN2LOdFNTq9Tie+5jZfYtxFV39+LYF3gmBcdTAwMTbeJVx1MDAxNYJQSorj3XrEnty37P7JsSlcdTAwMWT97NV7/VGvON5cdTAwMDXRXHUwMDA0wXxcdTAwMWPsXHUwMDE4YlxyYyqT18uAvaVTRNE02JWGTWOcsSlwc8FcdTAwMDRUUCDrXHUwMDAy9y/MXHUwMDA0Zj9cdTAwMThH82CG67KPb+3+/bFcclx1MDAwZnZunFx1MDAwN3v/sL01XHUwMDE3tlx1MDAxOVx1MDAwNVxigWVhe0zOYmZcdTAwMWMqXHUwMDExXHUwMDE4R1x1MDAxY4nCuE5/63RcXLf1Zrvnm1VAtkxDNlZ4X1x1MDAxY9mBrztdT/dcdTAwMTWaUtBNU9CN8LTpJlAqky9WgO5lXHUwMDAyMJ5n11x0Lq3PcKhcdTAwMTNcdTAwMGZcYq/WddvqfIxNVYRMJell4CZcdTAwMDXVu6bqMcIhXHUwMDFmu/d7x3pcbpG72VTvYPpjoFx1MDAwZSzl6oxusC3DSFx1MDAxYfOmXHUwMDEyQFfP9Fx1MDAwZopcdTAwMThh17eeLEfvXFwl5SvPXHUwMDFmjMlM/lAug0SCo8J65lx1MDAxY2/3t06R6Fx1MDAxY8BG42brXHUwMDAzXFw1/I9q81x1MDAwN2NEXHUwMDAzWIJJd4lgpPRcZoGF/aWmaVx1MDAxMEMvSiFiSskgR8q1XHUwMDEyZG1cdTAwMTQymMpeXHUwMDFmn+zsideL9zuj9tK6sE9/bDfS/aNIU2Jcbvma/thZzJTeYXFmXHUwMDEyWPmXWMaTtVwiZqJcdOM4yUycXHUwMDAzyVxiT8zsLI3JXHUwMDFm5ooyk7JcdTAwMTnpOkOp8rpcdTAwMTbXmaWQXHUwMDEzXHUwMDA3XGIoXHUwMDFkS7jhqyenXHUwMDEyXHUwMDE4XFyMnFx1MDAxYWbXXGbWyk4zTPwkO1xyXHUwMDA0LE9PMGFcdTAwMTEntI1wXGKQXHUwMDE0rHh4c/zs9smja183ru5Ojp73jWtcdTAwMTI0q01PhCtVo2Lc34v8QMQ1oMhhPKYuXHUwMDEz4kSfND3DXHUwMDFhnExcdTAwMTiMXHUwMDE0XHUwMDBlU02OO6dDveMq5FRcdTAwMTaArkDtyvHKx0n/ljx8Xlx1MDAwN96xW/NZv+18st1cbkY8kmRcdTAwMDFcdTAwMWQyjpVZ4ax4wJP+0lx1MDAxNadcdTAwMTVcdTAwMTVIZGBd+WJ0knCWzitcdTAwMDRPwzyFV6hcdTAwMDQq5GGr8MeqXHUwMDEz9Fx1MDAwMPAt/Mc1XHUwMDA018ouMyz0JLskxSzPMVhmptCgQFxicVxuOSusende//LkZGf39uiqVoeg3Ttv+8e/k2RwkZQxXHUwMDA1krOpnDGBQmOccrGqIIgypnHO8Vx1MDAxOJOMJY1cdTAwMDVcdTAwMWRPZY/CXCJcdTAwMDRcdTAwMTX/XHUwMDEwsVYtXHUwMDE0XHUwMDEwYonm0MLyoKRcdTAwMTRngpJcdTAwMTPJkVx1MDAwNKQ4XHUwMDFmyGOhO/unXHKf1M97bbttXl1cdTAwMWZUvZAhqFx1MDAwNlx1MDAwNaWEXHQ+XHUwMDE5mlx1MDAxM00uI7ubVcoomt1ccrP8gvLEq6wlNP++3Xpr2O32XHUwMDE2Mz6fejY4erg74uOuz3JD8/RcdTAwMGWr50IpK5KlM0x5XHUwMDBmXHUwMDAwwDlcXKj8Ua6oXHUwMDBipaxCltJQppElKM1yYnMsXHUwMDAwXHUwMDEwfFx1MDAxNTW/6vhQl4Hurzc2n2HlpzPHoYBcdTAwMGL4TSiborCQiOE5XHUwMDEyYW7t5NW8QPzItnuNeqv/dnyB33+vuvFcIsE5XHUwMDAxqZkwXCKVT1xuVqxspVx1MDAwMnRKwuhFrrve/iDfL8+O/Ju9k7bVMPZcdTAwMGZcdTAwMDS+P2uvkrTSO6xcdTAwMWVpIZpZgFGBP8CcUFA8xZU/zFx1MDAxNVUjXHUwMDE192eoXHUwMDExpVx1MDAxYV9xOrlY2C9cdFx1MDAxMpJSuM5s8lx1MDAxMIFwXHUwMDBlXHUwMDA0LsZYw3BcdTAwMWFo41x1MDAwM7py3pph+zOi/kjM8uyFWKazKFx1MDAxOEZcdTAwMDBcdTAwMTFQ3Fl8RkivfaKzXHUwMDFh3vGI34JXd82b/d9cdTAwMTlfkdlBP9Mog1x1MDAxNOGpJVx1MDAwNsroaVx1MDAwYkf8Lf1cdTAwMTFcdTAwMDCaXHUwMDE28XNccipdXHUwMDFh0/WR4il9R1igsPeoXHUwMDBmOKmHXGJcdTAwMTBlXHUwMDEwMV1fOXRcdTAwMTbP6PpcdTAwMTXbu7o/OFx1MDAwMSe39nZw+eRcdTAwMWScXs63WkxIXHRji7NcIp6BIDvBLJByJOZYXHUwMDE3mf7O6XBv+m63u9XWg2b794NewEzQh1x1MDAwZVx1MDAxM4aMrDTJTCmfXHUwMDA2fUqAxCRcdTAwMDJUKcl6k8xzXHUwMDAzcTG62Td1I0lcdTAwMWJrYJpcdTAwMTl2epJphlx1MDAxMpYnXHUwMDE5XG4zSVx1MDAwNlwiyKOca2Gtc+y3k8tcdTAwMTf48Irk+71OwFx1MDAxYjx9LpVZRkvSNzpb34CGXGKcjIRcIq9cdTAwMTAqfWNkXCJEKUE0XGKJR5OlXHUwMDExXHKZVjWcsohcciNcdTAwMTLSUWVcYuU2QPrdzsOWj/pta5/dP+3hXHUwMDA3ez5CkYrd4/dZVeDCM0vzkGEogUCgePyf/ta/nVJcbkCcZkJcXPlRy4D4XGZGSYF5SvxcdTAwMTKuhyFcdTAwMDCtmVDmXHUwMDA14mKEUnfdYM2EMsMmT1x1MDAxMspQwkVcYiUn56ZAiOZSujP60NxvdY1cdTAwMWYt1Kyf39qn/aB5U/FaJdOYXHUwMDEwJKVYSTHXplLf1ShWXCLMJcOYraBamc4xw1x1MDAxMsZcdTAwMWU7O3uxrf7lNjk6291BXHUwMDFl+Vx1MDAxMOk5t1x1MDAxMntcXFx1MDAxMGF8PbVQxrM3XHUwMDAzIE64ilx1MDAwN+fINNvPp/a2d3x0dF4zOvt35lPt8sWpei2Ua5hcdTAwMTE0UYn/XHUwMDE42H9ccirVXzhgX7RcdTAwMTgqiFRTQcSai6E7XHI9eDyy2s9cdTAwMTdcdTAwMTf711x1MDAwZl3y2q5jP1x1MDAxZOPF8soreuwsry+9w3lcdTAwMTRS4DCHumqvj+akq7lcdTAwMDBcdTAwMTBcdFx1MDAwNouHNPnDXFzZXCIrytRGTjW2XHUwMDA0bVxcSpVcdTAwMTVcYkokoGtdXHUwMDAxXVx1MDAwMoaL+Xzrr7LO4I9lV1lcdMkmP4wogpTC4jt0asfXzkO7Z+yC163vV/XWNdjln5WvXHUwMDBmQY1yXHRTNjVTJrRVr1x1MDAwYi1XZoWIXHUwMDAxiClfRVxuL4+4LpqH6PFwz3Vfnlxm9/Ni79Opi93F+XDpj53Fh+lcdTAwMWRWj1x1MDAwZrHIXFw8ijBcdTAwMDVcXMGmeFx1MDAxOSl/lKuqnSxTOznRhFgxXHUwMDE5XHUwMDE2q98qXHUwMDE2RGqq1rxVdc1cXPi76rczSKV0/TYz84hpls5cdTAwMTGBJZtrb3jt7HnLe39/dPmLrd95dYs9YiND59aSVZ/tgEpGNSogSFnmR2G4LUjmXHUwMDFm84FcdNZJ2W2rXHUwMDAwpejbXHUwMDE0/SGiXGaCSM5D1TNcdTAwMWW59Cdr37dcdTAwMGbPXHUwMDFmxPfDmjh8faa017r/sbRMXG6lXHUwMDAwr6/sXHUwMDE2blx1MDAxOH9cdTAwMWbPnSdthFx1MDAxOPvCyFx1MDAwNHTMVpBjIcY2yY+bh7FcdTAwMTdJ270+XHUwMDEwJtdcdTAwMTJcZlx1MDAwNjWNfrOPP1FcdTAwMDAkmPB5zvvJn+Zq2lx1MDAwMlxuNFxmXHUwMDA1Y1x1MDAwMmMsMJw0XHUwMDA3QoOYhVx1MDAwYiZouFtyNTaBizBcdTAwMDWFZbhcdTAwMWSR8OSBXHUwMDAx8cJcdTAwMGWoQYSEomGGpVx1MDAwMvykvVAuNCfhSq/57UUkZNn6hGCiVH2iXHUwMDFiRnU1yzEs50k1xibj1/lVXHUwMDA3XHUwMDA1yCXS1WYvlHJcdTAwMGJoTKrRXHUwMDEzMFxc8IeT5yGFY6F7USSiodCLYThcdTAwMWNGQeTwhpHp2jRcdTAwMWQjlmn8NfRusO3atlx1MDAxNahcdTAwMDE4dy0nmLwjeqPvoZ61TX1Kb9WTk22TXG7phU9cdTAwMWO3zvFvXHUwMDFiMWSjP0a///k19e6tTDxFrZNQilx1MDAxZvcl+f/cplx1MDAwNKHszJZEUir9wsUzW/lMVFFTwjV1V9pcdTAwMGVMisP1zFxmQ45BaFNXYkaksmScyF9cdTAwMWaeslSGKFx0XHUwMDE5XHUwMDAyXHUwMDEw8TCyXHUwMDAyaOq4XGbVICVgskTOa1x1MDAxMTtCldqW8vOXbUeApiw9UapcdTAwMDEg41xckZ9I3DQwI0IjXHUwMDAyRJ5jvv3IkiW/aDgmXHUwMDBilzzccqIgxaREdFpcdTAwMTaENUSHO1x1MDAwZqek+VvZrEzwhp8p2M5pszKTXHUwMDBmXHS+nNzwhChnhOHiqcGjz7u911Zw8456jkktv9WA9duKWywsNJZusZQnpFx0wfLPR1gsXHUwMDEwimWO3ZxYvtFSPlx1MDAwZVx1MDAxMFwiZM1lsVx1MDAwNVx1MDAwMpbcQGhVXHUwMDAx1mqPY1x1MDAxNJSBeUrVS1xusP7n/DOyUKbxr9RYK5G6mi9cdTAwMWazcLSVlKyct4SzXHUwMDE3okjCwpNw5ti1kj/7XHUwMDE1NT1UxV2/llx1MDAxMidXqlx1MDAwZk6skJpiWIakcth5cqf2Mk1cdTAwMTBcdTAwMTOaXHUwMDE4nLhcdTAwMWFcdTAwMTGPiD2QODOjUVxuOFx1MDAwZuXkTFwiknagXHUwMDA1XHUwMDA2UGBaJlVT5dArn87G3Fx1MDAxNFxiiVQ2XHUwMDFhcCYoJ3FcYj1yU5SXMtixkO8x/X0jrkwkhZ9pXGbN6b7k1jd5zuJcdTAwMWWoaF1IJIpnc9+E3fQ+3G6999yRXHUwMDA3dv2cg+1SlmSdp6lRXHJcdTAwMDCkQszpbC5ccjclrGg5W9FcdTAwMDNcYjFV1mE1S0fzPILT21OvfaVcdTAwMDb70rm+wVcnwdbxoZ3uXHUwMDExzFPIXFz6Y2dcdTAwMTUy0zss7r4oJlxy/cd5dlx1MDAwMeYqY1YskdyJMJX+4FhKguY4ai1/mCu6sEe56pmayFXYXFyBo1x1MDAxM1x1MDAxMMRcZofnXHUwMDExrfPgm1x1MDAxMlx1MDAxMFxczINe/7GGM3gj71jDL0Pl3dQ97zJQ4zZyStTUWMbw5eOx2nyzzPda9kl8X4a6XHUwMDFiKolcdTAwMTlOzF8/v/z8P1xiXCJcdTAwMWT6In0= StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset"},{"location":"tutorial/#custom-widgets","title":"Custom widgets","text":"

    We need a Stopwatch widget composed of the following child widgets:

    • A \"Start\" button
    • A \"Stop\" button
    • A \"Reset\" button
    • A time display

    Textual has a builtin Button widget which takes care of the first three components. All we need to build is the time display widget which will show the elapsed time and the stopwatch widget itself.

    Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.

    stopwatch02.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    We've imported two new widgets in this code: Button, which creates a clickable button, and Static which is a base class for a simple control. We've also imported ScrollableContainer from textual.containers which (as the name suggests) is a Widget which contains other widgets.

    We've defined an empty TimeDisplay widget by extending Static. We will flesh this out later.

    The Stopwatch widget class also extends Static. This class has a compose() method which yields child widgets, consisting of three Button objects and a single TimeDisplay object. These widgets will form the stopwatch in our sketch.

    "},{"location":"tutorial/#the-buttons","title":"The buttons","text":"

    The Button constructor takes a label to be displayed in the button (\"Start\", \"Stop\", or \"Reset\"). Additionally, some of the buttons set the following parameters:

    • id is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
    • variant is a string which selects a default style. The \"success\" variant makes the button green, and the \"error\" variant makes it red.
    "},{"location":"tutorial/#composing-the-widgets","title":"Composing the widgets","text":"

    To add widgets to our application we first need to yield them from the app's compose() method:

    The new line in StopwatchApp.compose() yields a single ScrollableContainer object which will create a scrolling list of stopwatches. When classes contain other widgets (like ScrollableContainer) they will typically accept their child widgets as positional arguments. We want to start the app with three stopwatches, so we construct three Stopwatch instances and pass them to the container's constructor.

    "},{"location":"tutorial/#the-unstyled-app","title":"The unstyled app","text":"

    Let's see what happens when we run stopwatch02.py.

    stopwatch02.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2586\u2586 Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any styles to our new widgets.

    "},{"location":"tutorial/#writing-textual-css","title":"Writing Textual CSS","text":"

    Every widget has a styles object with a number of attributes that impact how the widget will appear. Here's how you might set white text and a blue background for a widget:

    self.styles.background = \"blue\"\nself.styles.color = \"white\"\n

    While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.

    Info

    The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn.

    CSS makes it easy to iterate on the design of your app and enables live-editing \u2014 you can edit CSS and see the changes without restarting the app!

    Let's add a CSS file to our application.

    stopwatch03.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch03.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Adding the CSS_PATH class variable tells Textual to load the following file when the app starts:

    stopwatch03.tcss
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n\nTimeDisplay {\n    content-align: center middle;\n    text-opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n

    If we run the app now, it will look very different.

    stopwatch03.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    This app looks much more like our sketch. Let's look at how Textual uses stopwatch03.tcss to apply styles.

    "},{"location":"tutorial/#css-basics","title":"CSS basics","text":"

    CSS files contain a number of declaration blocks. Here's the first such block from stopwatch03.tcss again:

    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n

    The first line tells Textual that the styles should apply to the Stopwatch widget. The lines between the curly brackets contain the styles themselves.

    Here's how this CSS code changes how the Stopwatch widget is displayed.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT20pcdTAwMTb9nl/hYuZj3Ol9SdXUXHUwMDE0mLCEsIUlycx7lVx1MDAxMpKwXHUwMDE1ZMuxZFx1MDAxNr/Kf58rQazd2MZcdTAwMDYz9VRcdTAwMTRcdTAwMTi1lqu+5/Q993bLf71pNNaiu7679r6x5t7alu85XHUwMDAz62btbbz/2lx1MDAxZIRe0IMmmvxcdTAwMWZcdTAwMDbDgZ1cdTAwMWPZiaJ++P7du641uHKjvm/ZLrr2wqHlh9HQ8Vx1MDAwMmRcdTAwMDfdd17kdsN/x79cdTAwMGasrvuvftB1olx1MDAwMUpv0nRcdTAwMWQvXG5cdTAwMDb393J9t+v2olx1MDAxMK7+X/i/0fgr+Z2xbuDakdVr+25yQtKUXHUwMDFhKDUt7j1cYnqJsUxcdTAwMWLKJNVkfIBcdTAwMTduwu1cIteB1ksw2U1b4l1rp+fnV527b8cnrfbt94D7XHUwMDFi3uczkt710vP9k+jOT6xcblx1MDAwM3iYtC2MXHUwMDA2wZX7xXOiXHUwMDBltJLC/rqzXHUwMDA2wbDd6blhmDsn6Fu2XHUwMDE33cWPgMc773vgfSPdc1x1MDAxYvvHMIQ1M5ozJcYt8amUcyR1Zue9Ja3Ah55cdTAwMDdL/oGTLbXlwrKv2mBQz0mPsV2HO1Z6zM3D81x0KZFSiuXu2nG9dieKn1x1MDAwNGOkXHUwMDA1XHUwMDExkmbu7ia9ToRmUnKJ06eN79nfdVx1MDAxMlx1MDAwMPyZ7Zme89AzvaHvp2bGXHJcdTAwMWaKoMlcdTAwMDIn41Duj1pHN44z3Fx1MDAxMteYbOtmv+tvjVx1MDAxZieHMmswXGJu1sYtv1x1MDAxZT6lXHUwMDE2XHL7jnVcdTAwMGZcdTAwMWRcIiU2WmAhXHUwMDE11eN23+tdXHUwMDE1jfVcdTAwMDP7KkVbsvfX2zlQrjNAKKCcXHUwMDFiXGZcdTAwMDBQxkyN8rsvx1x1MDAxZlpmt7l7sVx1MDAxZH36XHUwMDFlXHUwMDA1m+vfv929JMpcdTAwMDEvj8CcYYaUMErmMJXAXFxiZLhghD5cculcdTAwMWPbWKgy0onEZYBLWcJ1XGZcYqYwXHUwMDExz4LrT+vOZcft2ups+1x1MDAwN93fOT883j2+rcZ15N5GL1xi6+T2XHUwMDE1iFaE1CFagT9h5FJ8akRP7o48ojuW3Vx1MDAxOVx1MDAwZdxcdTAwMTXAtOCIVmNaU8SfjuloYPXCvjVcdTAwMDBAVYzgplxma8pKsNaEY8IlN1x1MDAwYoN1NfI0JopzXHUwMDA2XHUwMDAxxcyAvNTDQS868UZxJ1Oc27tldT3/LuekXHUwMDA0ktA/J5E1iLI9XHUwMDE4unDL+Fx1MDAxYUTlXHUwMDBlXve9dozZNVx1MDAxYlx1MDAxZcJcdTAwMWTk4Fx1MDAxY3mgasZcdTAwMDd0PcfJjuA2WGDBNVx1MDAwN7vTjLzBwGt7Pcs/zVx1MDAxOTh/1JC4NmpcdTAwMDA8qVx1MDAxNJRjNTXJLs9bR82D/Vx1MDAxZnZcdTAwMTieXvMvrattdui9LMnUY1x1MDAxY+NGIaNAbpiSPFJcdTAwMDJRSvPkWzTJiGCIXHUwMDE0mTymXHUwMDFiXHUwMDEzqKDbXHUwMDFlWCdcdTAwMDWEXHUwMDEyLtXiSDcplvxs/Tw7bNODjf6BXGZcdTAwMGZYf32fd9ZfVSxcdTAwMTGM1eFcXCjCmOQzxJLJ3bGaMFx1MDAxN1x1MDAwNNfBXFxcdTAwMTOkXGIvwGzRMKeqjO6KYIKxXHUwMDExhDC6OO3/SDDheFx1MDAwNug9LZhg/D75QfmOXFx6SHlkVC6GlKyZ81x1MDAwN1x1MDAxNsNqxZvkXFxRo9X06VxiiUbHwaXeXHUwMDEyx9d476K5c9lum9FqpyNcdTAwMTJYVZlzS4qUNE/PRury7upsRJeYJsBcYmJohoPLjCBk3TlmXHUwMDAz22/rI4b90Z41+mzvT1x1MDAxNUHeTrrsK0re61wik8a6jijGKEO1Znp6okzs5lx1MDAxNc1yXHUwMDE0q6GKXHUwMDA2/bVcdTAwMDCqTFxmTFVsqYhLRIPWXHUwMDEykv8/xqXPbug+b5LzyHhejEj3XHUwMDA2zsUurmrDkCaKMqxcdJ6aXba5acqbj6PNnVx1MDAxZGNv9Jtfty82/VVnl2FcdTAwMDblKXTPLYPMMlx1MDAwNZ+syGbKtV5cbj4gYpFBaHVoJVx1MDAxYTGLwmpi6TmJXHUwMDE1XHUwMDA1/TpW5Vx1MDAxZaRIod/GTCTRfYisYpFcdTAwMTF1LDKGXG4lKZ9ezIU6tH7Y/fObs30pdLBcdTAwMWZcdTAwMWVccjtm5VlEXGZSxVpAQiRjXHUwMDEwgU4gT5xHmUgmjLjGPDdVMyZcdTAwMTU3SFx1MDAwYpFv/M0uzDCnRlxuNVx1MDAwM71SlfVcdTAwMWJcdTAwMTf0Yc+vWVk3n4rK9KI1iDa8nuP12sVT3J5T0+JbYdRcbrpdL1x1MDAwMjOOXHUwMDAyr1x1MDAxN1x1MDAxNY9IrrtcdTAwMWVjveNaJebAlbNtRVL04yvm5W76qZGiJvln/PnPt5VHN8uOTXZnfJpe4k3278xcdTAwMTRcdTAwMTZEXHUwMDE2945cdTAwMGJ9XHUwMDFjc1x1MDAwZVx1MDAxNJ9+XHUwMDEyNNjb/EG2etf+9o9j94BcdTAwMGbZXHUwMDA3vi9Xn8JcdTAwMTKp4pzj/VxmqkHw/MulMEFwXHUwMDAzrFx1MDAxNIa7S1xuv1N3pFTGSFx1MDAxMlx1MDAwM4iQ4H2IjCVcdTAwMTXKXHUwMDE4xEqhZyq1/83n5+NzvZPjrejeXHUwMDE5qZ1Ih1xuZoO2q2W2XHUwMDExJp5Mz1x1MDAxND9cdTAwMWaj9lx1MDAwNiNcdTAwMWJH+ESwy+1u6+Y/ncNR3/+48tRcdTAwMDZcdTAwMDZcdTAwMGLMXGYuRWcuXHUwMDExlpLjZZbwKYabK11ZwIemglnjSifXXHUwMDE0XHUwMDAw8vJcXJ5rNm0luJxrWyiRyy5NTvvtzFx1MDAwNTGXitpJXHSipJEwWkwvq+Uu3zsyh7fru4e7QeeL+DB0jtXqXHUwMDEzVyFVZEhcdTAwMTKTXHUwMDA1RphcdTAwMTZWXHUwMDBmLZy4ulxcX0qZW2IsXHUwMDAxLa20kS+upv9mbPnoXG5fXHUwMDE2zpuKqlx1MDAxM+c0uObFveNcdTAwMDUpkFx1MDAwNzKq9fRz5XpcdTAwMDN3j/btT9s3XHUwMDE3n4eR/rBtt1uXXHUwMDBi5qtjhVx1MDAxZHehhKVKIa6q8mAqXHUwMDEwK+Soi6/VUiSK0TxdUshcZsJJ0sRNsqW+eGCwwdhcdTAwMTBN8PMswFx1MDAxYfV1Z7RPvu58O1xuNvpdtqPWeVrkzKGuODfxdtJ1N7+FV2fOQXvnzmtcdTAwMWXuaOc6PO21XHUwMDE2Oucxy/gykU11VVmKa4lcdTAwMDTeoVxcSZmpOD3GpD1ztH973lx1MDAxYlx1MDAxZFx1MDAwN1tcdTAwMTY+3W3tnUaOs/pMMlx1MDAxYVx1MDAxOVMxXHUwMDFmXHUwMDBmXGJFy5asRFat7SqLVCmwoYw/05LFJzDmcWRcdTAwMTOuNGczIDtcdTAwMDXQPNXfTjDwRnF91m/41l0wrJlgqalcdTAwMDP77mWeOIupXHUwMDAyl42aSN/aYlx1MDAxMnRnXHUwMDFkf6VcdTAwMTJcdTAwMWNcdTAwMGJNplx1MDAwZoSTvb6iwpVcdTAwMWKGaHHRTHKqXHUwMDExyFx1MDAxMEKZXHUwMDAwclPOl8hhXCKRllx1MDAxNFBccsZcdTAwMDDCVVxu7kz6yVxihERcdTAwMDKySGtwSibbTFclXHUwMDBiaJJslpnNRata+EPFXFwzM1x1MDAwNVWbMezhpZHdKcRWwmZ7XHUwMDE4W9kkiEOPXHUwMDE4JrHmMZS1zlx1MDAxY9W2+snIjZhhXFxcdTAwMTCBwftClFx1MDAxZT0np59sXHUwMDEyQUxjqZigQihcdTAwMDXAqrZIYKyEhFxcimNOy+54VWW0WmTHW7NcZupcdTAwMTlFfq0sMfVcdTAwMGLOMSdEsVx1MDAxOfLxw+DO4k54ff1ldHeuw7NbbM72Vn1Yo0IjTngpXHUwMDFk58QgoYtLUVx1MDAxNz6gVa1cdTAwMTEsi1x1MDAxMiaJxmyRazFej9x+mighXHLb9f1G11x1MDAxYYAsWFx1MDAwNUGSN2g+MVwiMolegbWUKqownaH8PdnbK8paRlx1MDAxOeKCK+CEXHUwMDAxksr0ce+5S5ApTlx1MDAxZi/8XVx1MDAxMYyIVFxuXHUwMDEzXHUwMDA16TdE81xuLcI5wsVcdTAwMWH9mNLUyDjZmGVcdTAwMWTIypbW6lwi/uSIkFx1MDAxNSFcdTAwMThRKiBcdTAwMDVUgsVcdTAwMDE9M3c7XHUwMDBl+Vxu3aeOS1x1MDAxNlx1MDAxZnGsNYZwJjVcdTAwMDX8UJa+XHIyNoUhSVx1MDAxNNFcdTAwMTW2vCbRUYvgeGtmwLsgscFVffGfU1x1MDAxMtdcdTAwMTPN9Fx1MDAwYj+DT/3t06/d1sneh83RlzY3gm0uupq4+IWfXFwhSD1UbuI9XHUwMDE5skDQXHUwMDEyTJb84k06bTpcdTAwMWWjMmsgxiVcdTAwMTCl43l38zxqY1lcdTAwMGKa01x1MDAwNXDpczyT2uhbTjxcdTAwMWH90bOSLmpcXFxmoyjoVa+Ly1x1MDAxNGieZ13cIzbOp0iMqZ+Rh5QyjjczvFQ3XHUwMDE5XHUwMDEyK0ptjTWIXHUwMDBlI0g8y22yq3DvJYlGy13WTeM8XHUwMDFlayZULHziZVx1MDAxMGWySzBcdTAwMTGyXGKBQVx1MDAxOUllZEX5XHUwMDEz0ktqlHjR2lxi5UYuYr1NnVx1MDAxNphcdTAwMWM68rJcdTAwMDRzoaRkQlx1MDAwYkY1XHUwMDE1qVvHYoBgXHUwMDA0fVx1MDAxZft2PmUy+fs18taA3OVKa2ygg1x1MDAxNElfXHUwMDBiXHUwMDFlXHUwMDFiXHUwMDAzQZvGqVwieJFRzMTrLos060GdNJfwvCClQln9bI2iWlxiQXG6mu2x4cy/utz+aX8ynYNd7Xe3xGZw5lxmVn0441qh4mqEZCBjXHUwMDE0SUrwUpfSXHUwMDEzOZVS0VhcdTAwMTlccu5/nlx1MDAxN7q+hV/7srP58/bC73V+3uy3XHUwMDA33Y81Xy8xh1Dh0NnPJ1RShzTs2CN/9Lyw8c+LIFxiq2dtnl2lTDJwPolCSP1SXHUwMDA2oWFw4Wz696EnY2FFKS2kQFLG31x1MDAwYsMwXHUwMDAxzFx1MDAxNTNcdTAwMTCOlvviv0RExyNcdTAwMDcxXHUwMDFhXCKpIFx1MDAxNStcdTAwMWGMRIWv/lx1MDAxOL+8aSBlXHUwMDAyXHUwMDEx+ULC5IkknVKYTI5cdTAwMTRcdTAwMDVhXCJcdTAwMTTTWihKuMBgXHUwMDFlz1x1MDAxY/a7YFx1MDAwMsExKWBPXHUwMDE2Jq928XEtoOKtOcZSnSh483DhNavfP4nA1WPPXHUwMDAwtjznYVBNn27t2nNvNipeTL5MtnhcXEp6OKa/m+Dv15tf/1x1MDAwM1x1MDAxY9s3yyJ9 Start00:00:00.00Reset5 lineshorizontal layout1 cell margin1 cell paddingaround buttonsbackground coloris $boost
    • layout: horizontal aligns child widgets horizontally from left to right.
    • background: $boost sets the background color to $boost. The $ prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as \"blue\" or rgb(20,46,210).
    • height: 5 sets the height of our widget to 5 lines of text.
    • margin: 1 sets a margin of 1 cell around the Stopwatch widget to create a little space between widgets in the list.
    • min-width: 50 sets the minimum width of our widget to 50 cells.
    • padding: 1 sets a padding of 1 cell around the child widgets.

    Here's the rest of stopwatch03.tcss which contains further declaration blocks:

    TimeDisplay {\n    content-align: center middle;\n    opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n

    The TimeDisplay block aligns text to the center (content-align), fades it slightly (opacity), and sets its height (height) to 3 lines.

    The Button block sets the width (width) of buttons to 16 cells (character widths).

    The last 3 blocks have a slightly different format. When the declaration begins with a # then the styles will be applied to widgets with a matching \"id\" attribute. We've set an ID on the Button widgets we yielded in compose. For instance the first button has id=\"start\" which matches #start in the CSS.

    The buttons have a dock style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.

    You may have noticed that the stop button (#stop in the CSS) has display: none;. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is not running. Similarly, we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.

    "},{"location":"tutorial/#dynamic-css","title":"Dynamic CSS","text":"

    We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a started state with a Stop button. When a stopwatch is started it should also have a green background and bold text.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa1PiSFx1MDAxNP3ur7CYr5rp98OqrS1fOO4oXCLq6ri1ZYWkIZFAMFx0XHUwMDAzOOV/305wJYFEXHUwMDEwXHUwMDExXHUwMDE5Synp7vS96b6nz+nb/Wtjc7NcdTAwMTRccruqtLNZUlx1MDAwM8v0XFw7MPulrbj8p1xuQtfv6CqUfFx1MDAwZv1eYCUtnSjqhjtfv7bNoKWirmdayvjphj3TXHUwMDBio57t+oblt7+6kWqHf8afXHUwMDE1s63+6PptO1xujLGRbWW7kVx1MDAxZoxsKU+1VSdcbnXv/+jvm5u/ks+Ud4GyXCKz0/RU8kBSNXZcdTAwMTBTNlla8TuJs1x1MDAxMENcdTAwMDExxlx1MDAwMr20cMNcdTAwMDNtL1K2rm5on9W4Ji4qoW23o/reoSjzk6FPh+JQXHUwMDBl62OzXHLX8y6ioZe4XHUwMDE1+vptxnVhXHUwMDE0+C117dqRXHUwMDEz254oL3oq8HtNp6PCMPOM3zUtN1x1MDAxYcZlXHUwMDAwvJSOxmBnc1xcMohniFx1MDAwMlx1MDAwM0GJ9GtcbojoS138NELSoIhiXHRYqmbk0b7v6SnQXHUwMDFlfVx1MDAwMcnP2Ke6abWa2rGOPW5DgFx1MDAwNShcdTAwMWa36T+/J2XIwJJQnDbtKLfpRCPvXHJcdTAwMGVcdTAwMDVP2VbJ2ENcdTAwMDRcdTAwMDRHXHUwMDEwkfHUxFx1MDAxNrvHdlx1MDAxMlx1MDAwN/9Ojp5jXHUwMDA23edRKoXxl5S3saOHk0GUXHUwMDBlpNT87pFcdTAwMTCE/UFj96T5sCtxXHUwMDEz/+Wdf3/pK1x1MDAxM3VmXHUwMDEw+P3SS83T839j13pd21x1MDAxY0VcdTAwMTJkXGZxyKCk+u+l3nM7LV3Z6XneuMy3WuPgS0qftlx1MDAxNoh6wkFh1EtcdTAwMGUklZSLuaM+qNTR7vZcdL4/sitn1UrtR3hb21vzqGfAkFx1MDAwMFx0jFx1MDAwMJ6KekJcclx1MDAwMlx1MDAxOYD0vVHfMDV60HTU686ng52xqSiHUlxuXHUwMDAyQGqyVlx1MDAxMuWHu4d30VEzVJWWffvQOFx1MDAwNPBuu5dcdTAwMWblkVx1MDAxYUSpIN/K7zbTemteg5+HnYyfXHUwMDE5siBFsEFcYnKs10syN2peXHUwMDFm5SxqXHUwMDFj03J6gVpcdTAwMDfc0GLcMGbIZeAmXG7MTtg1XHUwMDAzXHUwMDFkqznYoTnYQXhcbjtSYqqdoWL52FlmXHUwMDFjjufb70RcdTAwMTfuY1x1MDAxMksgU1o22643zExZXHUwMDEyoNrTi8hPO2qGSltMVnGeabvruc04gEuWflx1MDAwN1x1MDAxNWRiO3K1rnpp0HZtO81cdTAwMTmWdsDUfVx1MDAwNsfzLPV+4Dbdjuldpv1bnKZQ6jUmaUpALUpcdTAwMDCZX5v1XHUwMDBlr1x1MDAwM+fS/SZJvyo6dXl601x1MDAxMt/Wm6VcYqVcdTAwMDaEnHHNxlNoo9JcdTAwMDBcdTAwMTJyyTL6aFx1MDAxMZZKfvKQhlxySIq0XHUwMDE51lx1MDAwYoEgOEebMY6JXlxiXHRbPvJeY63WXfPQbLdcdTAwMWQk78hjXHUwMDE5n4fSuyrQZsthrXyD68dakMhCXHUwMDE0SUxcdTAwMDSjLKUwZsHo9WFeU9pcIlruXHUwMDE1XHUwMDAySVNcdTAwMWFcdTAwMTV4MpiXTVtcdTAwMDRPY2iatqhcdTAwMDZcdTAwMWRcdTAwMTP4I7CzPqxcdTAwMDXATvJrZFx1MDAwN/TDyWtcdTAwMDZcdTAwMDNMklfazcU5jMvirVx1MDAxNlx1MDAxMZzogYfzi0bYP6uehFF9t7ZrXHUwMDFmPJRZ11xu67X1JjGGpcFFNoVcdTAwMTA/uc1cclx1MDAxZOlUvlx1MDAxM3VfLGVcdTAwMTPbnHeTJSZcdTAwMTHHXHUwMDA0xVxcolXvsS49//6x45fvavVqZ7/Svq5W9uvvYavfq9tZ3Jpv8Fxy3Eo5kzKlXHUwMDBlP4hbXHUwMDE5K1aoVCAuxVx1MDAxYsD9+iivKbUyQnLhXHKJISAj781cdTAwMWMuZy9IuVx1MDAxNrJcdTAwMWN/XHUwMDAwxpdcdTAwMTmB72PVmlxuVbRSPp1BRpN8OnJwcSalQFx1MDAxNGGNx0hcdTAwMTNcZr9cdTAwMDFrXHUwMDAz+O3+7j4kofqhRexpMPjR3f5MXCLFcyXqqeSMTjEpgsYyXHUwMDA0bCGVUsZcZs71ti5tIJOlXHUwMDE3dFwi6/N/nlx1MDAxZSMtcEBqXHUwMDExXFyJno1XXHUwMDFl+Fx1MDAwNuQtXHUwMDFllIyjoqDEXHUwMDE4MoTwXHUwMDFiUlx1MDAxNOft87NK1ynT/burRlx1MDAxN3lcdTAwMTXsW0frre6SRDqmlGTSXHUwMDEwSVTG26qJNOEyj47mTaJTXHUwMDBl9NKPwYrTXHUwMDExe41WebD7/Vie3J62L09cdTAwMDfgxqyU36/EfpduZ1x0vHyD6yfwaHHKX6+4klxuXCLnz528PsprKvCSlH8uwqGuWEJcdTAwMDJyXHUwMDE5XHUwMDEyXHUwMDBmaX2nXaFopYmTVUu8i8hcZlYr8WYw0nS+P3ZwcTYlpJBNIcNcdTAwMTJLXHUwMDFkb/NrvL9r29fSqdVOq4KU/dtcdTAwMDPn4LzZ/FxcuPG5Uv5cdTAwMTTg6VxylTCYpO9l01x1MDAxOVhbLOFPmdB2Kf6Ao7bXOOvCeri+8/f/dlx1MDAwNvXHq+F5w2pcdTAwMGWCo/dT4e/S7SyGzTe4flxmi3nhXHIsxiiIz3Hnh/zro7ymkI9cdTAwMGYnciGvXHRcdTAwMTZ+KLnOdyghpN5eM/BcdTAwMTH4Xlx1MDAxZm79rEOJXHUwMDE5JLXwoURcdTAwMTHcXGIovsNCMJGI0vlcdTAwMDXtzZk68Fx1MDAxYex+31x1MDAxOYKa1+Ku6Fx1MDAwNnTdXHUwMDA1LYlcdTAwMTMpOVlcdTAwMTQujUmVu3TAXHUwMDAxlFx1MDAwM7hpRiWAUckkXm3aXHUwMDA0QcRTm+pVXFxe6ZuR5eTjTeTjzVON6Fx1MDAxNbRlLsRkoZZ5kbybKiNnXHUwMDE2Qlx1MDAxNS1GXHUwMDE1RHqXKKVEb4BVs/5Ar1x1MDAxYSdcdTAwMDdu9fjspkrCI+ZcdTAwMDRrXHUwMDBmK6hcdTAwMTUqZtn7usmjnFx1MDAxOIxcIvyx5+tcdTAwMTLOhSwmXHUwMDEwkECI1e1cdTAwMTOJ5jLGqcSrRJbehil7czGEzWS0xTE26dZcYmtcdTAwMWLPKrRkdru6TVx1MDAxNDs3Qp6eXHUwMDFj135+/XHXpZ+u6u9cdTAwMTXfZNp4xm9cZlx1MDAxNFx1MDAxNU/Nr6eNp/9cdTAwMDDD6SGzIn0= Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch

    We can accomplish this with a CSS class. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.

    Here's the new CSS:

    stopwatch04.tcss
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    margin: 1;\n    min-width: 50;\n    padding: 1;\n}\n\nTimeDisplay {\n    content-align: center middle;\n    text-opacity: 60%;\n    height: 3;\n}\n\nButton {\n    width: 16;\n}\n\n#start {\n    dock: left;\n}\n\n#stop {\n    dock: left;\n    display: none;\n}\n\n#reset {\n    dock: right;\n}\n\n.started {\n    text-style: bold;\n    background: $success;\n    color: $text;\n}\n\n.started TimeDisplay {\n    text-opacity: 100%;\n}\n\n.started #start {\n    display: none\n}\n\n.started #stop {\n    display: block\n}\n\n.started #reset {\n    visibility: hidden\n}\n

    These new rules are prefixed with .started. The . indicates that .started refers to a CSS class called \"started\". The new styles will be applied only to widgets that have this CSS class.

    Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:

    .started #start {\n    display: none\n}\n

    The .started selector matches any widget with a \"started\" CSS class. While #start matches a child widget with an ID of \"start\". So it matches the Start button only for Stopwatches in a started state.

    The rule is \"display: none\" which tells Textual to hide the button.

    "},{"location":"tutorial/#manipulating-classes","title":"Manipulating classes","text":"

    Modifying a widget's CSS classes is a convenient way to update visuals without introducing a lot of messy display related code.

    You can add and remove CSS classes with the add_class() and remove_class() methods. We will use these methods to connect the started state to the Start / Stop buttons.

    The following code will start or stop the stopwatches in response to clicking a button.

    stopwatch04.py
    from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        if event.button.id == \"start\":\n            self.add_class(\"started\")\n        elif event.button.id == \"stop\":\n            self.remove_class(\"started\")\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay(\"00:00:00.00\")\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    The on_button_pressed method is an event handler. Event handlers are methods called by Textual in response to an event such as a key press, mouse click, etc. Event handlers begin with on_ followed by the name of the event they will handle. Hence on_button_pressed will handle the button pressed event.

    If you run stopwatch04.py now you will be able to toggle between the two states by clicking the first button:

    stopwatch04.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:00.00 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    "},{"location":"tutorial/#reactive-attributes","title":"Reactive attributes","text":"

    A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call refresh() to display new data. However, Textual prefers to do this automatically via reactive attributes.

    You can declare a reactive attribute with reactive. Let's use this feature to create a timer that displays elapsed time and keeps it updated.

    stopwatch05.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.set_interval(1 / 60, self.update_time)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update the time to the current time.\"\"\"\n        self.time = monotonic() - self.start_time\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        if event.button.id == \"start\":\n            self.add_class(\"started\")\n        elif event.button.id == \"stop\":\n            self.remove_class(\"started\")\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets for the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    We have added two reactive attributes to the TimeDisplay widget: start_time will contain the time (in seconds) the stopwatch was started, and time will contain the time to be displayed on the Stopwatch.

    Both attributes will be available on self as if you had assigned them in __init__. If you write to either of these attributes the widget will update automatically.

    Info

    The monotonic function in this example is imported from the standard library time module. It is similar to time.time but won't go backwards if the system clock is changed.

    The first argument to reactive may be a default value for the attribute or a callable that returns a default value. We set the default for start_time to the monotonic function which will be called to initialize the attribute with the current time when the TimeDisplay is added to the app. The time attribute has a simple float as the default, so self.time will be initialized to 0.

    The on_mount method is an event handler called when the widget is first added to the application (or mounted in Textual terminology). In this method we call set_interval() to create a timer which calls self.update_time sixty times a second. This update_time method calculates the time elapsed since the widget started and assigns it to self.time \u2014 which brings us to one of Reactive's super-powers.

    If you implement a method that begins with watch_ followed by the name of a reactive attribute, then the method will be called when the attribute is modified. Such methods are known as watch methods.

    Because watch_time watches the time attribute, when we update self.time 60 times a second we also implicitly call watch_time which converts the elapsed time to a string and updates the widget with a call to self.update. Because this happens automatically, we don't need to pass in an initial argument to TimeDisplay.

    The end result is that the Stopwatch widgets show the time elapsed since the widget was created:

    stopwatch05.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.07Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.07Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:03.07Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate stopwatches independently.

    "},{"location":"tutorial/#wiring-buttons","title":"Wiring buttons","text":"

    We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the TimeDisplay class.

    stopwatch06.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n    def start(self) -> None:\n        \"\"\"Method to start (or resume) time updating.\"\"\"\n        self.start_time = monotonic()\n        self.update_timer.resume()\n\n    def stop(self) -> None:\n        \"\"\"Method to stop the time display updating.\"\"\"\n        self.update_timer.pause()\n        self.total += monotonic() - self.start_time\n        self.time = self.total\n\n    def reset(self) -> None:\n        \"\"\"Method to reset the time display to zero.\"\"\"\n        self.total = 0\n        self.time = 0\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch04.tcss\"\n    BINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Called to add widgets to the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Here's a summary of the changes made to TimeDisplay.

    • We've added a total reactive attribute to store the total time elapsed between clicking the start and stop buttons.
    • The call to set_interval has grown a pause=True argument which starts the timer in pause mode (when a timer is paused it won't run until resume() is called). This is because we don't want the time to update until the user hits the start button.
    • The update_time method now adds total to the current time to account for the time between any previous clicks of the start and stop buttons.
    • We've stored the result of set_interval which returns a Timer object. We will use this later to resume the timer when we start the Stopwatch.
    • We've added start(), stop(), and reset() methods.

    In addition, the on_button_pressed method on Stopwatch has grown some code to manage the time display when the user clicks a button. Let's look at that in detail:

        def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n

    This code supplies missing features and makes our app useful. We've made the following changes.

    • The first line retrieves id attribute of the button that was pressed. We can use this to decide what to do in response.
    • The second line calls query_one to get a reference to the TimeDisplay widget.
    • We call the method on TimeDisplay that matches the pressed button.
    • We add the \"started\" class when the Stopwatch is started (self.add_class(\"started\")), and remove it (self.remove_class(\"started\")) when it is stopped. This will update the Stopwatch visuals via CSS.

    If you run stopwatch06.py you will be able to use the stopwatches independently.

    stopwatch06.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:10.14 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:06.07 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.

    "},{"location":"tutorial/#dynamic-widgets","title":"Dynamic widgets","text":"

    The Stopwatch app creates widgets when it starts via the compose method. We will also need to create new widgets while the app is running, and remove widgets we no longer need. We can do this by calling mount() to add a widget, and remove() to remove a widget.

    Let's use these methods to implement adding and removing stopwatches to our app.

    stopwatch.py
    from time import monotonic\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n\n    def start(self) -> None:\n        \"\"\"Method to start (or resume) time updating.\"\"\"\n        self.start_time = monotonic()\n        self.update_timer.resume()\n\n    def stop(self):\n        \"\"\"Method to stop the time display updating.\"\"\"\n        self.update_timer.pause()\n        self.total += monotonic() - self.start_time\n        self.time = self.total\n\n    def reset(self):\n        \"\"\"Method to reset the time display to zero.\"\"\"\n        self.total = 0\n        self.time = 0\n\n\nclass Stopwatch(Static):\n    \"\"\"A stopwatch widget.\"\"\"\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Event handler called when a button is pressed.\"\"\"\n        button_id = event.button.id\n        time_display = self.query_one(TimeDisplay)\n        if button_id == \"start\":\n            time_display.start()\n            self.add_class(\"started\")\n        elif button_id == \"stop\":\n            time_display.stop()\n            self.remove_class(\"started\")\n        elif button_id == \"reset\":\n            time_display.reset()\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Create child widgets of a stopwatch.\"\"\"\n        yield Button(\"Start\", id=\"start\", variant=\"success\")\n        yield Button(\"Stop\", id=\"stop\", variant=\"error\")\n        yield Button(\"Reset\", id=\"reset\")\n        yield TimeDisplay()\n\n\nclass StopwatchApp(App):\n    \"\"\"A Textual app to manage stopwatches.\"\"\"\n\n    CSS_PATH = \"stopwatch.tcss\"\n\n    BINDINGS = [\n        (\"d\", \"toggle_dark\", \"Toggle dark mode\"),\n        (\"a\", \"add_stopwatch\", \"Add\"),\n        (\"r\", \"remove_stopwatch\", \"Remove\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Called to add widgets to the app.\"\"\"\n        yield Header()\n        yield Footer()\n        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch(), id=\"timers\")\n\n    def action_add_stopwatch(self) -> None:\n        \"\"\"An action to add a timer.\"\"\"\n        new_stopwatch = Stopwatch()\n        self.query_one(\"#timers\").mount(new_stopwatch)\n        new_stopwatch.scroll_visible()\n\n    def action_remove_stopwatch(self) -> None:\n        \"\"\"Called to remove a timer.\"\"\"\n        timers = self.query(\"Stopwatch\")\n        if timers:\n            timers.last().remove()\n\n    def action_toggle_dark(self) -> None:\n        \"\"\"An action to toggle dark mode.\"\"\"\n        self.dark = not self.dark\n\n\nif __name__ == \"__main__\":\n    app = StopwatchApp()\n    app.run()\n

    Here's a summary of the changes:

    • The ScrollableContainer object in StopWatchApp grew a \"timers\" ID.
    • Added action_add_stopwatch to add a new stopwatch.
    • Added action_remove_stopwatch to remove a stopwatch.
    • Added keybindings for the actions.

    The action_add_stopwatch method creates and mounts a new stopwatch. Note the call to query_one() with a CSS selector of \"#timers\" which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in action_add_stopwatch calls scroll_visible() which will scroll the container to make the new Stopwatch visible (if required).

    The action_remove_stopwatch function calls query() with a CSS selector of \"Stopwatch\" which gets all the Stopwatch widgets. If there are stopwatches then the action calls last() to get the last stopwatch, and remove() to remove it.

    If you run stopwatch.py now you can add a new stopwatch with the A key and remove a stopwatch with R.

    stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Stop00:00:06.06 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Start00:00:00.00Reset \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

    "},{"location":"tutorial/#what-next","title":"What next?","text":"

    Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.

    Read the guide for the full details on how to build sophisticated TUI applications with Textual.

    "},{"location":"widget_gallery/","title":"Widgets","text":"

    Welcome to the Textual widget gallery.

    We have many more widgets planned, or you can build your own.

    Info

    Textual is a TUI framework. Everything below runs in the terminal.

    "},{"location":"widget_gallery/#button","title":"Button","text":"

    A simple button with a variety of semantic styles.

    Button reference

    ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DefaultDefault \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Primary!Primary! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Success!Success! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Warning!Warning! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Error!Error! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"widget_gallery/#checkbox","title":"Checkbox","text":"

    A classic checkbox control.

    Checkbox reference

    CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

    "},{"location":"widget_gallery/#collapsible","title":"Collapsible","text":"

    Content that may be toggled on and off by clicking a title.

    Collapsible reference

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    "},{"location":"widget_gallery/#contentswitcher","title":"ContentSwitcher","text":"

    A widget for containing and switching display between multiple child widgets.

    ContentSwitcher reference

    "},{"location":"widget_gallery/#datatable","title":"DataTable","text":"

    A powerful data table, with configurable cursors.

    DataTable reference

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    "},{"location":"widget_gallery/#digits","title":"Digits","text":"

    Display numbers in tall characters.

    Digits reference

    DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

    "},{"location":"widget_gallery/#directorytree","title":"DirectoryTree","text":"

    A tree view of files and folders.

    DirectoryTree reference

    DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.faq \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.git \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.github \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.ipynb_checkpoints \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.mypy_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.pytest_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.screenshot_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.vscode \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist\u2583\u2583 \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0.coverage

    "},{"location":"widget_gallery/#footer","title":"Footer","text":"

    A footer to display and interact with key bindings.

    Footer reference

    FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

    "},{"location":"widget_gallery/#header","title":"Header","text":"

    A header to display the app's title and subtitle.

    Header reference

    HeaderApp \u2b58HeaderApp

    "},{"location":"widget_gallery/#input","title":"Input","text":"

    A control to enter text.

    Input reference

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#label","title":"Label","text":"

    A simple text label.

    Label reference

    "},{"location":"widget_gallery/#listview","title":"ListView","text":"

    Display a list of items (items may be other widgets).

    ListView reference

    ListViewExample One Two Three

    "},{"location":"widget_gallery/#loadingindicator","title":"LoadingIndicator","text":"

    Display an animation while data is loading.

    LoadingIndicator reference

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    "},{"location":"widget_gallery/#log","title":"Log","text":"

    Display and update lines of text (such as from a file).

    Log reference

    LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    "},{"location":"widget_gallery/#markdownviewer","title":"MarkdownViewer","text":"

    Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).

    MarkdownViewer reference

    MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

    "},{"location":"widget_gallery/#markdown","title":"Markdown","text":"

    Display a markdown document.

    Markdown reference

    MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

    "},{"location":"widget_gallery/#optionlist","title":"OptionList","text":"

    Display a vertical list of options (options may be Rich renderables).

    OptionList reference

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u2581\u2581\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#placeholder","title":"Placeholder","text":"

    Display placeholder content while you are designing a UI.

    Placeholder reference

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

    "},{"location":"widget_gallery/#pretty","title":"Pretty","text":"

    Display a pretty-formatted Rich renderable.

    Pretty reference

    PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

    "},{"location":"widget_gallery/#progressbar","title":"ProgressBar","text":"

    A configurable progress bar with ETA and percentage complete.

    ProgressBar reference

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250150% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$50\u00a0received!

    "},{"location":"widget_gallery/#radiobutton","title":"RadioButton","text":"

    A simple radio button.

    RadioButton reference

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#radioset","title":"RadioSet","text":"

    A collection of radio buttons, that enforces uniqueness.

    RadioSet reference

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Pictur\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#richlog","title":"RichLog","text":"

    Display and update text in a scrolling panel.

    RichLog reference

    RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

    "},{"location":"widget_gallery/#rule","title":"Rule","text":"

    A rule widget to separate content, similar to a <hr> HTML tag.

    Rule reference

    HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

    "},{"location":"widget_gallery/#select","title":"Select","text":"

    Select from a number of possible options.

    Select reference

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#selectionlist","title":"SelectionList","text":"

    Select multiple values from a list of options.

    SelectionList reference

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"widget_gallery/#sparkline","title":"Sparkline","text":"

    Display numerical data.

    Sparkline reference

    SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"widget_gallery/#static","title":"Static","text":"

    Displays simple static content. Typically used as a base class.

    Static reference

    "},{"location":"widget_gallery/#switch","title":"Switch","text":"

    An on / off control, inspired by toggle buttons.

    Switch reference

    SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#tabs","title":"Tabs","text":"

    A row of tabs you can select with the mouse or navigate with keys.

    Tabs reference

    TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    "},{"location":"widget_gallery/#tabbedcontent","title":"TabbedContent","text":"

    A Combination of Tabs and ContentSwitcher to navigate static content.

    TabbedContent reference

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    "},{"location":"widget_gallery/#textarea","title":"TextArea","text":"

    A multi-line text area which supports syntax highlighting various languages.

    TextArea reference

    TextAreaExample \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adef\u00a0hello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258adef\u00a0goodbye(name):\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widget_gallery/#tree","title":"Tree","text":"

    A tree control with expandable nodes.

    Tree reference

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    "},{"location":"api/","title":"API","text":"

    This is a API-level reference to the Textual API. Click the links to your left (or in the burger menu) to open a reference for each module.

    If you are new to Textual, you may want to read the tutorial or guide first.

    "},{"location":"api/app/","title":"App","text":"

    Here you will find the App class, which is the base class for Textual apps.

    See app basics for how to build Textual apps.

    "},{"location":"api/app/#textual.app.AutopilotCallbackType","title":"AutopilotCallbackType module-attribute","text":"
    AutopilotCallbackType: TypeAlias = (\n    \"Callable[[Pilot[object]], Coroutine[Any, Any, None]]\"\n)\n

    Signature for valid callbacks that can be used to control apps.

    "},{"location":"api/app/#textual.app.CSSPathType","title":"CSSPathType module-attribute","text":"
    CSSPathType = Union[\n    str, PurePath, List[Union[str, PurePath]]\n]\n

    Valid ways of specifying paths to CSS files.

    "},{"location":"api/app/#textual.app.ActionError","title":"ActionError class","text":"

    Bases: Exception

    Base class for exceptions relating to actions.

    "},{"location":"api/app/#textual.app.ActiveModeError","title":"ActiveModeError class","text":"

    Bases: ModeError

    Raised when attempting to remove the currently active mode.

    "},{"location":"api/app/#textual.app.App","title":"App class","text":"
    def __init__(\n    self, driver_class=None, css_path=None, watch_css=False\n):\n

    Bases: Generic[ReturnType], DOMNode

    The base class for Textual Applications.

    Parameters Parameter Default Description driver_class Type[Driver] | None None

    Driver class or None to auto-detect. This will be used by some Textual tools.

    css_path CSSPathType | None None

    Path to CSS or None to use the CSS_PATH class variable. To load multiple CSS files, pass a list of strings or paths which will be loaded in order.

    watch_css bool False

    Reload CSS if the files changed. This is set automatically if you are using textual run with the dev switch.

    Raises Type Description CssPathError

    When the supplied CSS path(s) are an unexpected type.

    "},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS: str | None = '*'\n

    A selector to determine what to focus automatically when a screen is activated.

    The widget focused is the first that matches the given CSS selector. Setting to None or \"\" disables auto focus.

    "},{"location":"api/app/#textual.app.App.COMMANDS","title":"COMMANDS class-attribute","text":"
    COMMANDS: set[\n    type[Provider] | Callable[[], type[Provider]]\n] = {get_system_commands}\n

    Command providers used by the command palette.

    Should be a set of command.Provider classes.

    "},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
    CSS: str = ''\n

    Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, and therefore takes priority in the event of a specificity clash.

    "},{"location":"api/app/#textual.app.App.CSS_PATH","title":"CSS_PATH class-attribute","text":"
    CSS_PATH: CSSPathType | None = None\n

    File paths to load CSS from.

    "},{"location":"api/app/#textual.app.App.ENABLE_COMMAND_PALETTE","title":"ENABLE_COMMAND_PALETTE class-attribute","text":"
    ENABLE_COMMAND_PALETTE: bool = True\n

    Should the command palette be enabled for the application?

    "},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
    MODES: dict[str, str | Screen | Callable[[], Screen]] = {}\n

    Modes associated with the app and their base screens.

    The base screen is the screen at the bottom of the mode stack. You can think of it as the default screen for that stack. The base screens can be names of screens listed in SCREENS, Screen instances, or callables that return screens.

    Example
    class HelpScreen(Screen[None]):\n    ...\n\nclass MainAppScreen(Screen[None]):\n    ...\n\nclass MyApp(App[None]):\n    MODES = {\n        \"default\": \"main\",\n        \"help\": HelpScreen,\n    }\n\n    SCREENS = {\n        \"main\": MainAppScreen,\n    }\n\n    ...\n
    "},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
    SCREENS: dict[\n    str, Screen[Any] | Callable[[], Screen[Any]]\n] = {}\n

    Screens associated with the app for the lifetime of the app.

    "},{"location":"api/app/#textual.app.App.SUB_TITLE","title":"SUB_TITLE class-attribute instance-attribute","text":"
    SUB_TITLE: str | None = None\n

    A class variable to set the default sub-title for the application.

    To update the sub-title while the app is running, you can set the sub_title attribute. See also the Screen.SUB_TITLE attribute.

    "},{"location":"api/app/#textual.app.App.TITLE","title":"TITLE class-attribute instance-attribute","text":"
    TITLE: str | None = None\n

    A class variable to set the default title for the application.

    To update the title while the app is running, you can set the title attribute. See also the Screen.TITLE attribute.

    "},{"location":"api/app/#textual.app.App.app_focus","title":"app_focus class-attribute instance-attribute","text":"
    app_focus = Reactive(True, compute=False)\n

    Indicates if the app has focus.

    When run in the terminal, the app always has focus. When run in the web, the app will get focus when the terminal widget has focus.

    "},{"location":"api/app/#textual.app.App.app_resume_signal","title":"app_resume_signal instance-attribute","text":"
    app_resume_signal = Signal(self, 'app-resume')\n

    The signal that is published when the app is resumed after a suspend.

    When the app is resumed after a App.suspend call this signal will be published; subscribe to this signal to perform work after the app has resumed.

    "},{"location":"api/app/#textual.app.App.app_suspend_signal","title":"app_suspend_signal instance-attribute","text":"
    app_suspend_signal = Signal(self, 'app-suspend')\n

    The signal that is published when the app is suspended.

    When App.suspend is called this signal will be published; subscribe to this signal to perform work before the suspension takes place.

    "},{"location":"api/app/#textual.app.App.children","title":"children property","text":"
    children: Sequence['Widget']\n

    A view onto the app's immediate children.

    This attribute exists on all widgets. In the case of the App, it will only ever contain a single child, which will be the currently active screen.

    Returns Type Description Sequence['Widget']

    A sequence of widgets.

    "},{"location":"api/app/#textual.app.App.current_mode","title":"current_mode property","text":"
    current_mode: str\n

    The name of the currently active mode.

    "},{"location":"api/app/#textual.app.App.cursor_position","title":"cursor_position instance-attribute","text":"
    cursor_position = Offset(0, 0)\n

    The position of the terminal cursor in screen-space.

    This can be set by widgets and is useful for controlling the positioning of OS IME and emoji popup menus.

    "},{"location":"api/app/#textual.app.App.dark","title":"dark class-attribute instance-attribute","text":"
    dark: Reactive[bool] = Reactive(True, compute=False)\n

    Use a dark theme if True, otherwise use a light theme.

    Modify this attribute to switch between light and dark themes.

    Example
    self.app.dark = not self.app.dark  # Toggle dark mode\n
    "},{"location":"api/app/#textual.app.App.debug","title":"debug property","text":"
    debug: bool\n

    Is debug mode enabled?

    "},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
    focused: Widget | None\n

    The widget that is focused on the currently active screen, or None.

    Focused widgets receive keyboard input.

    Returns Type Description Widget | None

    The currently focused widget, or None if nothing is focused.

    "},{"location":"api/app/#textual.app.App.is_headless","title":"is_headless property","text":"
    is_headless: bool\n

    Is the driver running in 'headless' mode?

    Headless mode is used when running tests with run_test.

    "},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
    log: Logger\n

    The textual logger.

    Example
    self.log(\"Hello, World!\")\nself.log(self.tree)\n
    Returns Type Description Logger

    A Textual logger.

    "},{"location":"api/app/#textual.app.App.namespace_bindings","title":"namespace_bindings property","text":"
    namespace_bindings: dict[str, tuple[DOMNode, Binding]]\n

    Get currently active bindings.

    If no widget is focused, then app-level bindings are returned. If a widget is focused, then any bindings present in the active screen and app are merged and returned.

    This property may be used to inspect current bindings.

    Returns Type Description dict[str, tuple[DOMNode, Binding]]

    A mapping of keys onto pairs of nodes and bindings.

    "},{"location":"api/app/#textual.app.App.return_code","title":"return_code property","text":"
    return_code: int | None\n

    The return code with which the app exited.

    Non-zero codes indicate errors. A value of 1 means the app exited with a fatal error. If the app wasn't exited yet, this will be None.

    Example

    The return code can be used to exit the process via sys.exit.

    my_app.run()\nsys.exit(my_app.return_code)\n

    "},{"location":"api/app/#textual.app.App.return_value","title":"return_value property","text":"
    return_value: ReturnType | None\n

    The return value of the app, or None if it has not yet been set.

    The return value is set when calling exit.

    "},{"location":"api/app/#textual.app.App.screen","title":"screen property","text":"
    screen: Screen[object]\n

    The current active screen.

    Returns Type Description Screen[object]

    The currently active (visible) screen.

    Raises Type Description ScreenStackError

    If there are no screens on the stack.

    "},{"location":"api/app/#textual.app.App.screen_stack","title":"screen_stack property","text":"
    screen_stack: Sequence[Screen[Any]]\n

    A snapshot of the current screen stack.

    Returns Type Description Sequence[Screen[Any]]

    A snapshot of the current state of the screen stack.

    "},{"location":"api/app/#textual.app.App.scroll_sensitivity_x","title":"scroll_sensitivity_x instance-attribute","text":"
    scroll_sensitivity_x: float = 4.0\n

    Number of columns to scroll in the X direction with wheel or trackpad.

    "},{"location":"api/app/#textual.app.App.scroll_sensitivity_y","title":"scroll_sensitivity_y instance-attribute","text":"
    scroll_sensitivity_y: float = 2.0\n

    Number of lines to scroll in the Y direction with wheel or trackpad.

    "},{"location":"api/app/#textual.app.App.size","title":"size property","text":"
    size: Size\n

    The size of the terminal.

    Returns Type Description Size

    Size of the terminal.

    "},{"location":"api/app/#textual.app.App.sub_title","title":"sub_title class-attribute instance-attribute","text":"
    sub_title: Reactive[str] = (\n    self.SUB_TITLE if self.SUB_TITLE is not None else \"\"\n)\n

    The sub-title for the application.

    The initial value for sub_title will be set to the SUB_TITLE class variable if it exists, or an empty string if it doesn't.

    Sub-titles are typically used to show the high-level state of the app, such as the current mode, or path to the file being worked on.

    Assign a new value to this attribute to change the sub-title. The new value is always converted to string.

    "},{"location":"api/app/#textual.app.App.title","title":"title class-attribute instance-attribute","text":"
    title: Reactive[str] = (\n    self.TITLE\n    if self.TITLE is not None\n    else f\"{self.__class__.__name__}\"\n)\n

    The title for the application.

    The initial value for title will be set to the TITLE class variable if it exists, or the name of the app if it doesn't.

    Assign a new value to this attribute to change the title. The new value is always converted to string.

    "},{"location":"api/app/#textual.app.App.use_command_palette","title":"use_command_palette instance-attribute","text":"
    use_command_palette: bool = self.ENABLE_COMMAND_PALETTE\n

    A flag to say if the application should use the command palette.

    If set to False any call to action_command_palette will be ignored.

    "},{"location":"api/app/#textual.app.App.workers","title":"workers property","text":"
    workers: WorkerManager\n

    The worker manager.

    Returns Type Description WorkerManager

    An object to manage workers.

    "},{"location":"api/app/#textual.app.App.action_add_class","title":"action_add_class async","text":"
    def action_add_class(self, selector, class_name):\n

    An action to add a CSS class to the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to add the class to.

    class_name str required

    The class to add to the selected widget.

    "},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
    def action_back(self):\n

    An action to go back to the previous screen (pop the current screen).

    Note

    If there is no screen to go back to, this is a non-operation (in other words it's safe to call even if there are no other screens on the stack.)

    "},{"location":"api/app/#textual.app.App.action_bell","title":"action_bell async","text":"
    def action_bell(self):\n

    An action to play the terminal 'bell'.

    "},{"location":"api/app/#textual.app.App.action_check_bindings","title":"action_check_bindings async","text":"
    def action_check_bindings(self, key):\n

    An action to handle a key press using the binding system.

    Parameters Parameter Default Description key str required

    The key to process.

    "},{"location":"api/app/#textual.app.App.action_command_palette","title":"action_command_palette method","text":"
    def action_command_palette(self):\n

    Show the Textual command palette.

    "},{"location":"api/app/#textual.app.App.action_focus","title":"action_focus async","text":"
    def action_focus(self, widget_id):\n

    An action to focus the given widget.

    Parameters Parameter Default Description widget_id str required

    ID of widget to focus.

    "},{"location":"api/app/#textual.app.App.action_focus_next","title":"action_focus_next method","text":"
    def action_focus_next(self):\n

    An action to focus the next widget.

    "},{"location":"api/app/#textual.app.App.action_focus_previous","title":"action_focus_previous method","text":"
    def action_focus_previous(self):\n

    An action to focus the previous widget.

    "},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
    def action_pop_screen(self):\n

    An action to remove the topmost screen and makes the new topmost screen active.

    "},{"location":"api/app/#textual.app.App.action_push_screen","title":"action_push_screen async","text":"
    def action_push_screen(self, screen):\n

    An action to push a new screen on to the stack and make it active.

    Parameters Parameter Default Description screen str required

    Name of the screen.

    "},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
    def action_quit(self):\n

    An action to quit the app as soon as possible.

    "},{"location":"api/app/#textual.app.App.action_remove_class","title":"action_remove_class async","text":"
    def action_remove_class(self, selector, class_name):\n

    An action to remove a CSS class from the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to remove the class from.

    class_name str required

    The class to remove from the selected widget.

    "},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot method","text":"
    def action_screenshot(self, filename=None, path='./'):\n

    This action will save an SVG file containing the current contents of the screen.

    Parameters Parameter Default Description filename str | None None

    Filename of screenshot, or None to auto-generate.

    path str './'

    Path to directory. Defaults to current working directory.

    "},{"location":"api/app/#textual.app.App.action_suspend_process","title":"action_suspend_process method","text":"
    def action_suspend_process(self):\n

    Suspend the process into the background.

    Note

    On Unix and Unix-like systems a SIGTSTP is sent to the application's process. Currently on Windows and when running under Textual Web this is a non-operation.

    "},{"location":"api/app/#textual.app.App.action_switch_mode","title":"action_switch_mode async","text":"
    def action_switch_mode(self, mode):\n

    An action that switches to the given mode..

    "},{"location":"api/app/#textual.app.App.action_switch_screen","title":"action_switch_screen async","text":"
    def action_switch_screen(self, screen):\n

    An action to switch screens.

    Parameters Parameter Default Description screen str required

    Name of the screen.

    "},{"location":"api/app/#textual.app.App.action_toggle_class","title":"action_toggle_class async","text":"
    def action_toggle_class(self, selector, class_name):\n

    An action to toggle a CSS class on the selected widget.

    Parameters Parameter Default Description selector str required

    Selects the widget to toggle the class on.

    class_name str required

    The class to toggle on the selected widget.

    "},{"location":"api/app/#textual.app.App.action_toggle_dark","title":"action_toggle_dark method","text":"
    def action_toggle_dark(self):\n

    An action to toggle dark mode.

    "},{"location":"api/app/#textual.app.App.add_mode","title":"add_mode method","text":"
    def add_mode(self, mode, base_screen):\n

    Adds a mode and its corresponding base screen to the app.

    Parameters Parameter Default Description mode str required

    The new mode.

    base_screen str | Screen | Callable[[], Screen] required

    The base screen associated with the given mode.

    Raises Type Description InvalidModeError

    If the name of the mode is not valid/duplicated.

    "},{"location":"api/app/#textual.app.App.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    See the guide for how to use the animation system.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update method","text":"
    def batch_update(self):\n

    A context manager to suspend all repaints until the end of the batch.

    "},{"location":"api/app/#textual.app.App.begin_capture_print","title":"begin_capture_print method","text":"
    def begin_capture_print(self, target, stdout=True, stderr=True):\n

    Capture content that is printed (or written to stdout / stderr).

    If printing is captured, the target will be sent an events.Print message.

    Parameters Parameter Default Description target MessageTarget required

    The widget where print content will be sent.

    stdout bool True

    Capture stdout.

    stderr bool True

    Capture stderr.

    "},{"location":"api/app/#textual.app.App.bell","title":"bell method","text":"
    def bell(self):\n

    Play the console 'bell'.

    For terminals that support a bell, this typically makes a notification or error sound. Some terminals may make no sound or display a visual bell indicator, depending on configuration.

    "},{"location":"api/app/#textual.app.App.bind","title":"bind method","text":"
    def bind(\n    self,\n    keys,\n    action,\n    *,\n    description=\"\",\n    show=True,\n    key_display=None\n):\n

    Bind a key to an action.

    Parameters Parameter Default Description keys str required

    A comma separated list of keys, i.e.

    action str required

    Action to bind to.

    description str ''

    Short description of action.

    show bool True

    Show key in UI.

    key_display str | None None

    Replacement text for key, or None to use default.

    "},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread method","text":"
    def call_from_thread(self, callback, *args, **kwargs):\n

    Run a callable from another thread, and return the result.

    Like asyncio apps in general, Textual apps are not thread-safe. If you call methods or set attributes on Textual objects from a thread, you may get unpredictable results.

    This method will ensure that your code runs within the correct context.

    Tip

    Consider using post_message which is also thread-safe.

    Parameters Parameter Default Description callback Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]] required

    A callable to run.

    *args Any ()

    Arguments to the callback.

    **kwargs Any {}

    Keyword arguments for the callback.

    Raises Type Description RuntimeError

    If the app isn't running or if this method is called from the same thread where the app is running.

    Returns Type Description CallThreadReturnType

    The result of the callback.

    "},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse method","text":"
    def capture_mouse(self, widget):\n

    Send all mouse events to the given widget or disable mouse capture.

    Parameters Parameter Default Description widget Widget | None required

    If a widget, capture mouse event, or None to end mouse capture.

    "},{"location":"api/app/#textual.app.App.check_bindings","title":"check_bindings async","text":"
    def check_bindings(self, key, priority=False):\n

    Handle a key press.

    This method is used internally by the bindings system, but may be called directly if you wish to simulate a key being pressed.

    Parameters Parameter Default Description key str required

    A key.

    priority bool False

    If True check from App down, otherwise from focused up.

    Returns Type Description bool

    True if the key was handled by a binding, otherwise False

    "},{"location":"api/app/#textual.app.App.clear_notifications","title":"clear_notifications method","text":"
    def clear_notifications(self):\n

    Clear all the current notifications.

    "},{"location":"api/app/#textual.app.App.compose","title":"compose method","text":"
    def compose(self):\n

    Yield child widgets for a container.

    This method should be implemented in a subclass.

    "},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print method","text":"
    def end_capture_print(self, target):\n

    End capturing of prints.

    Parameters Parameter Default Description target MessageTarget required

    The widget that was capturing prints.

    "},{"location":"api/app/#textual.app.App.exit","title":"exit method","text":"
    def exit(self, result=None, return_code=0, message=None):\n

    Exit the app, and return the supplied result.

    Parameters Parameter Default Description result ReturnType | None None

    Return value.

    return_code int 0

    The return code. Use non-zero values for error codes.

    message RenderableType | None None

    Optional message to display on exit.

    "},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot method","text":"
    def export_screenshot(self, *, title=None):\n

    Export an SVG screenshot of the current screen.

    See also save_screenshot which writes the screenshot to a file.

    Parameters Parameter Default Description title str | None None

    The title of the exported screenshot or None to use app title.

    "},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id method","text":"
    def get_child_by_id(self, id, expect_type=None):\n

    Get the first child (immediate descendent) of this DOMNode with the given ID.

    Parameters Parameter Default Description id str required

    The ID of the node to search for.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or use None to apply no type restriction.

    Returns Type Description ExpectType | Widget

    The first child of this node with the specified ID.

    Raises Type Description NoMatches

    If no children could be found for this ID.

    WrongType

    If the wrong type was found.

    "},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type method","text":"
    def get_child_by_type(self, expect_type):\n

    Get a child of a give type.

    Parameters Parameter Default Description expect_type type[ExpectType] required

    The type of the expected child.

    Raises Type Description NoMatches

    If no valid child is found.

    Returns Type Description ExpectType

    A widget.

    "},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables method","text":"
    def get_css_variables(self):\n

    Get a mapping of variables used to pre-populate CSS.

    May be implemented in a subclass to add new CSS variables.

    Returns Type Description dict[str, str]

    A mapping of variable name to value.

    "},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class method","text":"
    def get_driver_class(self):\n

    Get a driver class for this platform.

    This method is called by the constructor, and unlikely to be required when building a Textual app.

    Returns Type Description Type[Driver]

    A Driver class which manages input and display.

    "},{"location":"api/app/#textual.app.App.get_key_display","title":"get_key_display method","text":"
    def get_key_display(self, key):\n

    For a given key, return how it should be displayed in an app (e.g. in the Footer widget). By key, we refer to the string used in the \"key\" argument for a Binding instance. By overriding this method, you can ensure that keys are displayed consistently throughout your app, without needing to add a key_display to every binding.

    Parameters Parameter Default Description key str required

    The binding key string.

    Returns Type Description str

    The display string for the input key.

    "},{"location":"api/app/#textual.app.App.get_loading_widget","title":"get_loading_widget method","text":"
    def get_loading_widget(self):\n

    Get a widget to be used as a loading indicator.

    Extend this method if you want to display the loading state a little differently.

    Returns Type Description Widget

    A widget to display a loading state.

    "},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen method","text":"
    def get_screen(self, screen):\n

    Get an installed screen.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    Raises Type Description KeyError

    If the named screen doesn't exist.

    Returns Type Description Screen

    A screen instance.

    "},{"location":"api/app/#textual.app.App.get_widget_at","title":"get_widget_at method","text":"
    def get_widget_at(self, x, y):\n

    Get the widget under the given coordinates.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description tuple[Widget, Region]

    The widget and the widget's screen region.

    "},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id method","text":"
    def get_widget_by_id(self, id, expect_type=None):\n

    Get the first descendant widget with the given ID.

    Performs a breadth-first search rooted at the current screen. It will not return the Screen if that matches the ID. To get the screen, use self.screen.

    Parameters Parameter Default Description id str required

    The ID to search for in the subtree

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type. Defaults to None.

    Returns Type Description ExpectType | Widget

    The first descendant encountered with this ID.

    Raises Type Description NoMatches

    if no children could be found for this ID

    WrongType

    if the wrong type was found.

    "},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen method","text":"
    def install_screen(self, screen, name):\n

    Install a screen.

    Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. Note that you don't need to install a screen to use it. See push_screen or switch_screen to make a new screen current.

    Parameters Parameter Default Description screen Screen required

    Screen to install.

    name str required

    Unique name to identify the screen.

    Raises Type Description ScreenError

    If the screen can't be installed.

    Returns Type Description None

    An awaitable that awaits the mounting of the screen and its children.

    "},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted method","text":"
    def is_mounted(self, widget):\n

    Check if a widget is mounted.

    Parameters Parameter Default Description widget Widget required

    A widget.

    Returns Type Description bool

    True of the widget is mounted.

    "},{"location":"api/app/#textual.app.App.is_screen_installed","title":"is_screen_installed method","text":"
    def is_screen_installed(self, screen):\n

    Check if a given screen has been installed.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    Returns Type Description bool

    True if the screen is currently installed,

    "},{"location":"api/app/#textual.app.App.mount","title":"mount method","text":"
    def mount(self, *widgets, before=None, after=None):\n

    Mount the given widgets relative to the app's screen.

    Parameters Parameter Default Description *widgets Widget ()

    The widget(s) to mount.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all method","text":"
    def mount_all(self, widgets, *, before=None, after=None):\n

    Mount widgets from an iterable.

    Parameters Parameter Default Description widgets Iterable[Widget] required

    An iterable of widgets.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/app/#textual.app.App.notify","title":"notify method","text":"
    def notify(\n    self,\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=Notification.timeout\n):\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters Parameter Default Description message str required

    The message for the notification.

    title str ''

    The title for the notification.

    severity SeverityLevel 'information'

    The severity of the notification.

    timeout float Notification.timeout

    The timeout for the notification.

    The notify method is used to create an application-wide notification, shown in a Toast, normally originating in the bottom right corner of the display.

    Notifications can have the following severity levels:

    • information
    • warning
    • error

    The default is information.

    Example
    # Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n    \"Now witness the firepower of this fully \"\n    \"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\n    title=\"Possible trap detected\",\n    severity=\"warning\",\n)\n\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n
    "},{"location":"api/app/#textual.app.App.panic","title":"panic method","text":"
    def panic(self, *renderables):\n

    Exits the app and display error message(s).

    Used in response to unexpected errors. For a more graceful exit, see the exit method.

    Parameters Parameter Default Description *renderables RenderableType ()

    Text or Rich renderable(s) to display on exit.

    "},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen method","text":"
    def pop_screen(self):\n

    Pop the current screen from the stack, and switch to the previous screen.

    Returns Type Description Screen[object]

    The screen that was replaced.

    "},{"location":"api/app/#textual.app.App.post_display_hook","title":"post_display_hook method","text":"
    def post_display_hook(self):\n

    Called immediately after a display is done. Used in tests.

    "},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen method","text":"
    def push_screen(\n    self, screen, callback=None, wait_for_dismiss=False\n):\n

    Push a new screen on the screen stack, making it the current screen.

    Parameters Parameter Default Description screen Screen[ScreenResultType] | str required

    A Screen instance or the name of an installed screen.

    callback ScreenResultCallbackType[ScreenResultType] | None None

    An optional callback function that will be called if the screen is dismissed with a result.

    wait_for_dismiss bool False

    If True, awaiting this method will return the dismiss value from the screen. When set to False, awaiting this method will wait for the screen to be mounted. Note that wait_for_dismiss should only be set to True when running in a worker.

    Raises Type Description NoActiveWorker

    If using wait_for_dismiss outside of a worker.

    Returns Type Description AwaitMount | asyncio.Future[ScreenResultType]

    An optional awaitable that awaits the mounting of the screen and its children, or an asyncio Future to await the result of the screen.

    "},{"location":"api/app/#textual.app.App.push_screen_wait","title":"push_screen_wait async","text":"
    def push_screen_wait(self, screen):\n

    Push a screen and wait for the result (received from Screen.dismiss).

    Note that this method may only be called when running in a worker.

    Parameters Parameter Default Description screen Screen[ScreenResultType] | str required

    A screen or the name of an installed screen.

    Returns Type Description ScreenResultType | Any

    The screen's result.

    "},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css method","text":"
    def refresh_css(self, animate=True):\n

    Refresh CSS.

    Parameters Parameter Default Description animate bool True

    Also execute CSS animations.

    "},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode method","text":"
    def remove_mode(self, mode):\n

    Removes a mode from the app.

    Screens that are running in the stack of that mode are scheduled for pruning.

    Parameters Parameter Default Description mode str required

    The mode to remove. It can't be the active mode.

    Raises Type Description ActiveModeError

    If trying to remove the active mode.

    UnknownModeError

    If trying to remove an unknown mode.

    "},{"location":"api/app/#textual.app.App.run","title":"run method","text":"
    def run(self, *, headless=False, size=None, auto_pilot=None):\n

    Run the app.

    Parameters Parameter Default Description headless bool False

    Run in headless mode (no output).

    size tuple[int, int] | None None

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    auto_pilot AutopilotCallbackType | None None

    An auto pilot coroutine.

    Returns Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
    def run_action(self, action, default_namespace=None):\n

    Perform an action.

    Actions are typically associated with key bindings, where you wouldn't need to call this method manually.

    Parameters Parameter Default Description action str | ActionParseResult required

    Action encoded in a string.

    default_namespace object | None None

    Namespace to use if not provided in the action, or None to use app.

    Returns Type Description bool

    True if the event has been handled.

    "},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
    def run_async(\n    self, *, headless=False, size=None, auto_pilot=None\n):\n

    Run the app asynchronously.

    Parameters Parameter Default Description headless bool False

    Run in headless mode (no output).

    size tuple[int, int] | None None

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    auto_pilot AutopilotCallbackType | None None

    An auto pilot coroutine.

    Returns Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
    def run_test(\n    self,\n    *,\n    headless=True,\n    size=(80, 24),\n    tooltips=False,\n    notifications=False,\n    message_hook=None\n):\n

    An asynchronous context manager for testing apps.

    Tip

    See the guide for testing Textual apps.

    Use this to run your app in \"headless\" mode (no output) and drive the app via a Pilot object.

    Example
    async with app.run_test() as pilot:\n    await pilot.click(\"#Button.ok\")\n    assert ...\n
    Parameters Parameter Default Description headless bool True

    Run in headless mode (no output or input).

    size tuple[int, int] | None (80, 24)

    Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

    tooltips bool False

    Enable tooltips when testing.

    notifications bool False

    Enable notifications when testing.

    message_hook Callable[[Message], None] | None None

    An optional callback that will be called each time any message arrives at any message pump in the app.

    "},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot method","text":"
    def save_screenshot(\n    self, filename=None, path=\"./\", time_format=None\n):\n

    Save an SVG screenshot of the current screen.

    Parameters Parameter Default Description filename str | None None

    Filename of SVG screenshot, or None to auto-generate a filename with the date and time.

    path str './'

    Path to directory for output. Defaults to current working directory.

    time_format str | None None

    Date and time format to use if filename is None. Defaults to a format like ISO 8601 with some reserved characters replaced with underscores.

    Returns Type Description str

    Filename of screenshot.

    "},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus method","text":"
    def set_focus(self, widget, scroll_visible=True):\n

    Focus (or unfocus) a widget. A focused widget will receive key events first.

    Parameters Parameter Default Description widget Widget | None required

    Widget to focus.

    scroll_visible bool True

    Scroll widget in to view.

    "},{"location":"api/app/#textual.app.App.stop_animation","title":"stop_animation async","text":"
    def stop_animation(self, attribute, complete=True):\n

    Stop an animation on an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute whose animation should be stopped.

    complete bool True

    Should the animation be set to its final value?

    Note

    If there is no animation scheduled or running, this is a no-op.

    "},{"location":"api/app/#textual.app.App.suspend","title":"suspend method","text":"
    def suspend(self):\n

    A context manager that temporarily suspends the app.

    While inside the with block, the app will stop reading input and emitting output. Other applications will have full control of the terminal, configured as it was before the app started running. When the with block ends, the application will start reading input and emitting output again.

    Example
    with self.suspend():\n    os.system(\"emacs -nw\")\n
    Raises Type Description SuspendNotSupported

    If the environment doesn't support suspending.

    Note

    Suspending the application is currently only supported on Unix-like operating systems and Microsoft Windows. Suspending is not supported in Textual Web.

    "},{"location":"api/app/#textual.app.App.switch_mode","title":"switch_mode method","text":"
    def switch_mode(self, mode):\n

    Switch to a given mode.

    Parameters Parameter Default Description mode str required

    The mode to switch to.

    Returns Type Description AwaitMount

    An optionally awaitable object which waits for the screen associated with the mode to be mounted.

    Raises Type Description UnknownModeError

    If trying to switch to an unknown mode.

    "},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen method","text":"
    def switch_screen(self, screen):\n

    Switch to another screen by replacing the top of the screen stack with a new screen.

    Parameters Parameter Default Description screen Screen | str required

    Either a Screen object or screen name (the name argument when installed).

    "},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen method","text":"
    def uninstall_screen(self, screen):\n

    Uninstall a screen.

    If the screen was not previously installed then this method is a null-op. Uninstalling a screen allows Textual to delete it when it is popped or switched. Note that uninstalling a screen is only required if you have previously installed it with install_screen. Textual will also uninstall screens automatically on exit.

    Parameters Parameter Default Description screen Screen | str required

    The screen to uninstall or the name of a installed screen.

    Returns Type Description str | None

    The name of the screen that was uninstalled, or None if no screen was uninstalled.

    "},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles method","text":"
    def update_styles(self, node):\n

    Immediately update the styles of this node and all descendant nodes.

    Should be called whenever CSS classes / pseudo classes change. For example, when you hover over a button, the :hover pseudo class will be added, and this method is called to apply the corresponding :hover styles.

    "},{"location":"api/app/#textual.app.App.validate_sub_title","title":"validate_sub_title method","text":"
    def validate_sub_title(self, sub_title):\n

    Make sure the sub-title is set to a string.

    "},{"location":"api/app/#textual.app.App.validate_title","title":"validate_title method","text":"
    def validate_title(self, title):\n

    Make sure the title is set to a string.

    "},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark method","text":"
    def watch_dark(self, dark):\n

    Watches the dark bool.

    This method handles the transition between light and dark mode when you change the dark attribute.

    "},{"location":"api/app/#textual.app.AppError","title":"AppError class","text":"

    Bases: Exception

    Base class for general App related exceptions.

    "},{"location":"api/app/#textual.app.InvalidModeError","title":"InvalidModeError class","text":"

    Bases: ModeError

    Raised if there is an issue with a mode name.

    "},{"location":"api/app/#textual.app.ModeError","title":"ModeError class","text":"

    Bases: Exception

    Base class for exceptions related to modes.

    "},{"location":"api/app/#textual.app.ScreenError","title":"ScreenError class","text":"

    Bases: Exception

    Base class for exceptions that relate to screens.

    "},{"location":"api/app/#textual.app.ScreenStackError","title":"ScreenStackError class","text":"

    Bases: ScreenError

    Raised when trying to manipulate the screen stack incorrectly.

    "},{"location":"api/app/#textual.app.SuspendNotSupported","title":"SuspendNotSupported class","text":"

    Bases: Exception

    Raised if suspending the application is not supported.

    This exception is raised if App.suspend is called while the application is running in an environment where this isn't supported.

    "},{"location":"api/app/#textual.app.UnknownModeError","title":"UnknownModeError class","text":"

    Bases: ModeError

    Raised when attempting to use a mode that is not known.

    "},{"location":"api/app/#textual.app.get_system_commands","title":"get_system_commands function","text":"
    def get_system_commands():\n

    Callable to lazy load the system commands.

    Returns Type Description type[SystemCommands]

    System commands class.

    "},{"location":"api/await_complete/","title":"Await complete","text":""},{"location":"api/await_complete/#textual.await_complete.AwaitComplete","title":"AwaitComplete class","text":"
    def __init__(self, *coroutines):\n

    An 'optionally-awaitable' object.

    Parameters Parameter Default Description coroutines Coroutine[Any, Any, Any] ()

    One or more coroutines to execute.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.exception","title":"exception property","text":"
    exception: BaseException | None\n

    An exception if it occurred in any of the coroutines.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.is_done","title":"is_done property","text":"
    is_done: bool\n

    Returns True if the task has completed.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.nothing","title":"nothing classmethod","text":"
    def nothing(cls):\n

    Returns an already completed instance of AwaitComplete.

    "},{"location":"api/await_remove/","title":"Await remove","text":"

    An optionally awaitable object returned by methods that remove widgets.

    "},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove class","text":"
    def __init__(self, finished_flag, task):\n

    An awaitable returned by a method that removes DOM nodes.

    Returned by Widget.remove and DOMQuery.remove.

    Parameters Parameter Default Description finished_flag Event required

    The asyncio event to wait on.

    task Task required

    The task which does the remove (required to keep a reference).

    "},{"location":"api/binding/","title":"Binding","text":"

    A binding maps a key press on to an action.

    See bindings in the guide for details.

    "},{"location":"api/binding/#textual.binding.Binding","title":"Binding class","text":"

    The configuration of a key binding.

    "},{"location":"api/binding/#textual.binding.Binding.action","title":"action instance-attribute","text":"
    action: str\n

    Action to bind to.

    "},{"location":"api/binding/#textual.binding.Binding.description","title":"description class-attribute instance-attribute","text":"
    description: str = ''\n

    Description of action.

    "},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
    key: str\n

    Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.

    "},{"location":"api/binding/#textual.binding.Binding.key_display","title":"key_display class-attribute instance-attribute","text":"
    key_display: str | None = None\n

    How the key should be shown in footer.

    "},{"location":"api/binding/#textual.binding.Binding.priority","title":"priority class-attribute instance-attribute","text":"
    priority: bool = False\n

    Enable priority binding for this key.

    "},{"location":"api/binding/#textual.binding.Binding.show","title":"show class-attribute instance-attribute","text":"
    show: bool = True\n

    Show the action in Footer, or False to hide.

    "},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError class","text":"

    Bases: Exception

    A binding related error.

    "},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding class","text":"

    Bases: Exception

    Binding key is in an invalid format.

    "},{"location":"api/binding/#textual.binding.NoBinding","title":"NoBinding class","text":"

    Bases: Exception

    A binding was not found.

    "},{"location":"api/cache/","title":"Cache","text":"

    Containers to implement caching.

    These are used in Textual to avoid recalculating expensive operations, such as rendering.

    "},{"location":"api/cache/#textual.cache.FIFOCache","title":"FIFOCache class","text":"
    def __init__(self, maxsize):\n

    Bases: Generic[CacheKey, CacheValue]

    A simple cache that discards the first added key when full (First In First Out).

    This has a lower overhead than LRUCache, but won't manage a working set as efficiently. It is most suitable for a cache with a relatively low maximum size that is not expected to do many lookups.

    Parameters Parameter Default Description maxsize int required

    Maximum size of cache before discarding items.

    "},{"location":"api/cache/#textual.cache.FIFOCache.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.FIFOCache.get","title":"get method","text":"
    def get(self, key, default=None):\n

    Get a value from the cache, or return a default if the key is not present.

    Parameters Parameter Default Description key CacheKey required

    Key

    default DefaultValue | None None

    Default to return if key is not present.

    Returns Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.FIFOCache.keys","title":"keys method","text":"
    def keys(self):\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.FIFOCache.set","title":"set method","text":"
    def set(self, key, value):\n

    Set a value.

    Parameters Parameter Default Description key CacheKey required

    Key.

    value CacheValue required

    Value.

    "},{"location":"api/cache/#textual.cache.LRUCache","title":"LRUCache class","text":"
    def __init__(self, maxsize):\n

    Bases: Generic[CacheKey, CacheValue]

    A dictionary-like container with a maximum size.

    If an additional item is added when the LRUCache is full, the least recently used key is discarded to make room for the new item.

    The implementation is similar to functools.lru_cache, which uses a (doubly) linked list to keep track of the most recently used items.

    Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference to the previous entry, and NEXT is a reference to the next value.

    Note that stdlib's @lru_cache is implemented in C and faster! It's best to use @lru_cache where you are caching things that are fairly quick and called many times. Use LRUCache where you want increased flexibility and you are caching slow operations where the overhead of the cache is a small fraction of the total processing time.

    Parameters Parameter Default Description maxsize int required

    Maximum size of the cache, before old items are discarded.

    "},{"location":"api/cache/#textual.cache.LRUCache.maxsize","title":"maxsize writable property","text":"
    maxsize: int\n
    "},{"location":"api/cache/#textual.cache.LRUCache.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.LRUCache.discard","title":"discard method","text":"
    def discard(self, key):\n

    Discard item in cache from key.

    Parameters Parameter Default Description key CacheKey required

    Cache key.

    "},{"location":"api/cache/#textual.cache.LRUCache.get","title":"get method","text":"
    def get(self, key, default=None):\n

    Get a value from the cache, or return a default if the key is not present.

    Parameters Parameter Default Description key CacheKey required

    Key

    default DefaultValue | None None

    Default to return if key is not present.

    Returns Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.LRUCache.grow","title":"grow method","text":"
    def grow(self, maxsize):\n

    Grow the maximum size to at least maxsize elements.

    Parameters Parameter Default Description maxsize int required

    New maximum size.

    "},{"location":"api/cache/#textual.cache.LRUCache.keys","title":"keys method","text":"
    def keys(self):\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.LRUCache.set","title":"set method","text":"
    def set(self, key, value):\n

    Set a value.

    Parameters Parameter Default Description key CacheKey required

    Key.

    value CacheValue required

    Value.

    "},{"location":"api/color/","title":"Color","text":"

    This module contains a powerful Color class which Textual uses to manipulate colors.

    "},{"location":"api/color/#textual.color--named-colors","title":"Named colors","text":"

    The following named colors are used by the parse method.

    colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"api/color/#textual.color.BLACK","title":"BLACK module-attribute","text":"
    BLACK: Final = Color(0, 0, 0)\n

    A constant for pure black.

    "},{"location":"api/color/#textual.color.WHITE","title":"WHITE module-attribute","text":"
    WHITE: Final = Color(255, 255, 255)\n

    A constant for pure white.

    "},{"location":"api/color/#textual.color.Color","title":"Color class","text":"

    Bases: NamedTuple

    A class to represent a color.

    Colors are stored as three values representing the degree of red, green, and blue in a color, and a fourth \"alpha\" value which defines where the color lies on a gradient of opaque to transparent.

    Example
    >>> from textual.color import Color\n>>> color = Color.parse(\"red\")\n>>> color\nColor(255, 0, 0)\n>>> color.darken(0.5)\nColor(98, 0, 0)\n>>> color + Color.parse(\"green\")\nColor(0, 128, 0)\n>>> color_with_alpha = Color(100, 50, 25, 0.5)\n>>> color_with_alpha\nColor(100, 50, 25, a=0.5)\n>>> color + color_with_alpha\nColor(177, 25, 12)\n
    "},{"location":"api/color/#textual.color.Color.a","title":"a class-attribute instance-attribute","text":"
    a: float = 1.0\n

    Alpha (opacity) component in range 0 to 1.

    "},{"location":"api/color/#textual.color.Color.b","title":"b instance-attribute","text":"
    b: int\n

    Blue component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
    brightness: float\n

    The human perceptual brightness.

    A value of 1 is returned for pure white, and 0 for pure black. Other colors lie on a gradient between the two extremes.

    "},{"location":"api/color/#textual.color.Color.clamped","title":"clamped property","text":"
    clamped: Color\n

    A clamped color (this color with all values in expected range).

    "},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
    css: str\n

    The color in CSS RGB or RGBA form.

    For example, \"rgb(10,20,30)\" for an RGB color, or \"rgb(50,70,80,0.5)\" for an RGBA color.

    "},{"location":"api/color/#textual.color.Color.g","title":"g instance-attribute","text":"
    g: int\n

    Green component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
    hex: str\n

    The color in CSS hex form, with 6 digits for RGB, and 8 digits for RGBA.

    For example, \"#46b3de\" for an RGB color, or \"#3342457f\" for a color with alpha.

    "},{"location":"api/color/#textual.color.Color.hex6","title":"hex6 property","text":"
    hex6: str\n

    The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.

    For example, \"#46b3de\".

    "},{"location":"api/color/#textual.color.Color.hsl","title":"hsl property","text":"
    hsl: HSL\n

    This color in HSL format.

    HSL color is an alternative way of representing a color, which can be used in certain color calculations.

    Returns Type Description HSL

    Color encoded in HSL format.

    "},{"location":"api/color/#textual.color.Color.inverse","title":"inverse property","text":"
    inverse: Color\n

    The inverse of this color.

    Returns Type Description Color

    Inverse color.

    "},{"location":"api/color/#textual.color.Color.is_transparent","title":"is_transparent property","text":"
    is_transparent: bool\n

    Is the color transparent (i.e. has 0 alpha)?

    "},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
    monochrome: Color\n

    A monochrome version of this color.

    Returns Type Description Color

    The monochrome (black and white) version of this color.

    "},{"location":"api/color/#textual.color.Color.normalized","title":"normalized property","text":"
    normalized: tuple[float, float, float]\n

    A tuple of the color components normalized to between 0 and 1.

    Returns Type Description tuple[float, float, float]

    Normalized components.

    "},{"location":"api/color/#textual.color.Color.r","title":"r instance-attribute","text":"
    r: int\n

    Red component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.rgb","title":"rgb property","text":"
    rgb: tuple[int, int, int]\n

    The red, green, and blue color components as a tuple of ints.

    "},{"location":"api/color/#textual.color.Color.rich_color","title":"rich_color property","text":"
    rich_color: RichColor\n

    This color encoded in Rich's Color class.

    Returns Type Description RichColor

    A color object as used by Rich.

    "},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
    def blend(self, destination, factor, alpha=None):\n

    Generate a new color between two colors.

    This method calculates a new color on a gradient. The position on the gradient is given by factor, which is a float between 0 and 1, where 0 is the original color, and 1 is the destination color. A value of gradient between the two extremes produces a color somewhere between the two end points.

    Parameters Parameter Default Description destination Color required

    Another color.

    factor float required

    A blend factor, 0 -> 1.

    alpha float | None None

    New alpha for result.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.darken","title":"darken cached","text":"
    def darken(self, amount, alpha=None):\n

    Darken the color by a given amount.

    Parameters Parameter Default Description amount float required

    Value between 0-1 to reduce luminance by.

    alpha float | None None

    Alpha component for new color or None to copy alpha.

    Returns Type Description Color

    New color.

    "},{"location":"api/color/#textual.color.Color.from_hsl","title":"from_hsl classmethod","text":"
    def from_hsl(cls, h, s, l):\n

    Create a color from HLS components.

    Parameters Parameter Default Description h float required

    Hue.

    l float required

    Lightness.

    s float required

    Saturation.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.from_rich_color","title":"from_rich_color classmethod","text":"
    def from_rich_color(cls, rich_color):\n

    Create a new color from Rich's Color class.

    Parameters Parameter Default Description rich_color RichColor required

    An instance of Rich color.

    Returns Type Description Color

    A new Color instance.

    "},{"location":"api/color/#textual.color.Color.get_contrast_text","title":"get_contrast_text cached","text":"
    def get_contrast_text(self, alpha=0.95):\n

    Get a light or dark color that best contrasts this color, for use with text.

    Parameters Parameter Default Description alpha float 0.95

    An alpha value to apply to the result.

    Returns Type Description Color

    A new color, either an off-white or off-black.

    "},{"location":"api/color/#textual.color.Color.lighten","title":"lighten method","text":"
    def lighten(self, amount, alpha=None):\n

    Lighten the color by a given amount.

    Parameters Parameter Default Description amount float required

    Value between 0-1 to increase luminance by.

    alpha float | None None

    Alpha component for new color or None to copy alpha.

    Returns Type Description Color

    New color.

    "},{"location":"api/color/#textual.color.Color.multiply_alpha","title":"multiply_alpha method","text":"
    def multiply_alpha(self, alpha):\n

    Create a new color, multiplying the alpha by a constant.

    Parameters Parameter Default Description alpha float required

    A value to multiple the alpha by (expected to be in the range 0 to 1).

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.parse","title":"parse cached classmethod","text":"
    def parse(cls, color_text):\n

    Parse a string containing a named color or CSS-style color.

    Colors may be parsed from the following formats:

    • Text beginning with a # is parsed as a hexadecimal color code, where R, G, B, and A must be hexadecimal digits (0-9A-F):

      • #RGB
      • #RGBA
      • #RRGGBB
      • #RRGGBBAA
    • Alternatively, RGB colors can also be specified in the format that follows, where R, G, and B must be numbers between 0 and 255 and A must be a value between 0 and 1:

      • rgb(R,G,B)
      • rgb(R,G,B,A)
    • The HSL model can also be used, with a syntax similar to the above, if H is a value between 0 and 360, S and L are percentages, and A is a value between 0 and 1:

      • hsl(H,S,L)
      • hsla(H,S,L,A)

    Any other formats will raise a ColorParseError.

    Parameters Parameter Default Description color_text str | Color required

    Text with a valid color format. Color objects will be returned unmodified.

    Raises Type Description ColorParseError

    If the color is not encoded correctly.

    Returns Type Description Color

    Instance encoding the color specified by the argument.

    "},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha method","text":"
    def with_alpha(self, alpha):\n

    Create a new color with the given alpha.

    Parameters Parameter Default Description alpha float required

    New value for alpha.

    Returns Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError class","text":"
    def __init__(self, message, suggested_color=None):\n

    Bases: Exception

    A color failed to parse.

    Parameters Parameter Default Description message str required

    The error message

    suggested_color str | None None

    A close color we can suggest.

    "},{"location":"api/color/#textual.color.Gradient","title":"Gradient class","text":"
    def __init__(self, *stops):\n

    Defines a color gradient.

    A gradient is defined by a sequence of \"stops\" consisting of a float and a color. The stop indicate the color at that point on a spectrum between 0 and 1.

    Parameters Parameter Default Description stops tuple[float, Color] ()

    A colors stop.

    Raises Type Description ValueError

    If any stops are missing (must be at least a stop for 0 and 1).

    "},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color method","text":"
    def get_color(self, position):\n

    Get a color from the gradient at a position between 0 and 1.

    Positions that are between stops will return a blended color.

    Parameters Parameter Default Description position float required

    A number between 0 and 1, where 0 is the first stop, and 1 is the last.

    Returns Type Description Color

    A color.

    "},{"location":"api/color/#textual.color.HSL","title":"HSL class","text":"

    Bases: NamedTuple

    A color in HLS (Hue, Saturation, Lightness) format.

    "},{"location":"api/color/#textual.color.HSL.css","title":"css property","text":"
    css: str\n

    HSL in css format.

    "},{"location":"api/color/#textual.color.HSL.h","title":"h instance-attribute","text":"
    h: float\n

    Hue in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSL.l","title":"l instance-attribute","text":"
    l: float\n

    Lightness in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSL.s","title":"s instance-attribute","text":"
    s: float\n

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV","title":"HSV class","text":"

    Bases: NamedTuple

    A color in HSV (Hue, Saturation, Value) format.

    "},{"location":"api/color/#textual.color.HSV.h","title":"h instance-attribute","text":"
    h: float\n

    Hue in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.s","title":"s instance-attribute","text":"
    s: float\n

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
    v: float\n

    Value un range 0 to 1.

    "},{"location":"api/color/#textual.color.Lab","title":"Lab class","text":"

    Bases: NamedTuple

    A color in CIE-L*ab format.

    "},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
    L: float\n

    Lightness in range 0 to 100.

    "},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
    a: float\n

    A axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.Lab.b","title":"b instance-attribute","text":"
    b: float\n

    B axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb function","text":"
    def lab_to_rgb(lab, alpha=1.0):\n

    Convert a CIE-L*ab color to RGB.

    Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

    "},{"location":"api/color/#textual.color.rgb_to_lab","title":"rgb_to_lab function","text":"
    def rgb_to_lab(rgb):\n

    Convert an RGB color to the CIE-L*ab format.

    Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

    "},{"location":"api/command/","title":"Command","text":"

    The Textual command palette.

    See the guide on the Command Palette for full details.

    "},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
    Hits: TypeAlias = AsyncIterator[Hit]\n

    Return type for the command provider's search method.

    "},{"location":"api/command/#textual.command.Command","title":"Command class","text":"
    def __init__(self, prompt, command, id=None, disabled=False):\n

    Bases: Option

    Class that holds a command in the CommandList.

    Parameters Parameter Default Description prompt RenderableType required

    The prompt for the option.

    command Hit required

    The details of the command associated with the option.

    id str | None None

    The optional ID for the option.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"api/command/#textual.command.Command.command","title":"command instance-attribute","text":"
    command = command\n

    The details of the command associated with the option.

    "},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput class","text":"

    Bases: Input

    The command palette input control.

    "},{"location":"api/command/#textual.command.CommandList","title":"CommandList class","text":"

    Bases: OptionList

    The command palette command list.

    "},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette class","text":"
    def __init__(self):\n

    Bases: _SystemModalScreen[CallbackType]

    The Textual command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"ctrl+end, shift+end\",\n        \"command_list('last')\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+home, shift+home\",\n        \"command_list('first')\",\n        show=False,\n    ),\n    Binding(\"down\", \"cursor_down\", show=False),\n    Binding(\"escape\", \"escape\", \"Exit the command palette\"),\n    Binding(\n        \"pagedown\", \"command_list('page_down')\", show=False\n    ),\n    Binding(\n        \"pageup\", \"command_list('page_up')\", show=False\n    ),\n    Binding(\"up\", \"command_list('cursor_up')\", show=False),\n]\n
    Key(s) Description ctrl+end, shift+end Jump to the last available commands. ctrl+home, shift+home Jump to the first available commands. down Navigate down through the available commands. escape Exit the command palette. pagedown Navigate down a page through the available commands. pageup Navigate up a page through the available commands. up Navigate up through the available commands."},{"location":"api/command/#textual.command.CommandPalette.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"command-palette--help-text\",\n    \"command-palette--highlight\",\n}\n
    Class Description command-palette--help-text Targets the help text of a matched command. command-palette--highlight Targets the highlights of a matched command."},{"location":"api/command/#textual.command.CommandPalette.run_on_select","title":"run_on_select class-attribute","text":"
    run_on_select: bool = True\n

    A flag to say if a command should be run when selected by the user.

    If True then when a user hits Enter on a command match in the result list, or if they click on one with the mouse, the command will be selected and run. If set to False the input will be filled with the command and then Enter should be pressed on the keyboard or the 'go' button should be pressed.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
    def is_open(app):\n

    Is the command palette current open?

    Parameters Parameter Default Description app App required

    The app to test.

    Returns Type Description bool

    True if the command palette is currently open, False if not.

    "},{"location":"api/command/#textual.command.Hit","title":"Hit class","text":"

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
    command: IgnoreReturnCallbackType\n

    The function to call when the command is chosen.

    "},{"location":"api/command/#textual.command.Hit.help","title":"help class-attribute instance-attribute","text":"
    help: str | None = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
    match_display: RenderableType\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
    score: float\n

    The score of the command hit.

    The value should be between 0 (no match) and 1 (complete match).

    "},{"location":"api/command/#textual.command.Hit.text","title":"text class-attribute instance-attribute","text":"
    text: str | None = None\n

    The command text associated with the hit, as plain text.

    If match_display is not simple text, this attribute should be provided by the Provider object.

    "},{"location":"api/command/#textual.command.Matcher","title":"Matcher class","text":"
    def __init__(\n    self, query, *, match_style=None, case_sensitive=False\n):\n

    A fuzzy matcher.

    Parameters Parameter Default Description query str required

    A query as typed in by the user.

    match_style Style | None None

    The style to use to highlight matched portions of a string.

    case_sensitive bool False

    Should matching be case sensitive?

    "},{"location":"api/command/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive: bool\n

    Is this matcher case sensitive?

    "},{"location":"api/command/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_style: Style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/command/#textual.fuzzy.Matcher.query","title":"query property","text":"
    query: str\n

    The query string to look for.

    "},{"location":"api/command/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern: str\n

    The regular expression pattern built from the query.

    "},{"location":"api/command/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
    def highlight(self, candidate):\n

    Highlight the candidate with the fuzzy match.

    Parameters Parameter Default Description candidate str required

    The candidate string to match against the query.

    Returns Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/command/#textual.fuzzy.Matcher.match","title":"match method","text":"
    def match(self, candidate):\n

    Match the candidate against the query.

    Parameters Parameter Default Description candidate str required

    Candidate string to match against the query.

    Returns Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/command/#textual.command.Provider","title":"Provider class","text":"
    def __init__(self, screen, match_style=None):\n

    Bases: ABC

    Base class for command palette command providers.

    To create new command provider, inherit from this class and implement search.

    Parameters Parameter Default Description screen Screen[Any] required

    A reference to the active screen.

    "},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
    app: App[object]\n

    A reference to the application.

    "},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
    focused: Widget | None\n

    The currently-focused widget in the currently-active screen in the application.

    If no widget has focus this will be None.

    "},{"location":"api/command/#textual.command.Provider.match_style","title":"match_style property","text":"
    match_style: Style | None\n

    The preferred style to use when highlighting matching portions of the match_display.

    "},{"location":"api/command/#textual.command.Provider.screen","title":"screen property","text":"
    screen: Screen[object]\n

    The currently-active screen in the application.

    "},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher method","text":"
    def matcher(self, user_input, case_sensitive=False):\n

    Create a fuzzy matcher for the given user input.

    Parameters Parameter Default Description user_input str required

    The text that the user has input.

    case_sensitive bool False

    Should matching be case sensitive?

    Returns Type Description Matcher

    A fuzzy matcher object for matching against candidate hits.

    "},{"location":"api/command/#textual.command.Provider.search","title":"search async abstractmethod","text":"
    def search(self, query):\n

    A request to search for commands relevant to the given query.

    Parameters Parameter Default Description query str required

    The user input to be matched.

    Yields:

    Type Description Hits

    Instances of Hit.

    "},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
    def shutdown(self):\n

    Called when the Provider is shutdown.

    Use this method to perform an cleanup, if required.

    "},{"location":"api/command/#textual.command.Provider.startup","title":"startup async","text":"
    def startup(self):\n

    Called after the Provider is initialized, but before any calls to search.

    "},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon class","text":"

    Bases: Static

    Widget for displaying a search icon before the command input.

    "},{"location":"api/command/#textual.command.SearchIcon.icon","title":"icon class-attribute instance-attribute","text":"
    icon: var[str] = var(\n    Emoji.replace(\":magnifying_glass_tilted_right:\")\n)\n

    The icon to display.

    "},{"location":"api/containers/","title":"Containers","text":"

    Container widgets for quick styling.

    With the exception of Center and Middle containers will fill all of the space in the parent widget.

    "},{"location":"api/containers/#textual.containers.Center","title":"Center class","text":"

    Bases: Widget

    A container which aligns children on the X axis.

    "},{"location":"api/containers/#textual.containers.Container","title":"Container class","text":"

    Bases: Widget

    Simple container widget, with vertical layout.

    "},{"location":"api/containers/#textual.containers.Grid","title":"Grid class","text":"

    Bases: Widget

    A container with grid layout.

    "},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal class","text":"

    Bases: Widget

    A container with horizontal layout and no scrollbars.

    "},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll class","text":"

    Bases: ScrollableContainer

    A container with horizontal layout and an automatic scrollbar on the Y axis.

    "},{"location":"api/containers/#textual.containers.Middle","title":"Middle class","text":"

    Bases: Widget

    A container which aligns children on the Y axis.

    "},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer class","text":"

    Bases: Widget

    A scrollable container with vertical layout, and auto scrollbars on both axis.

    "},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"up\", \"scroll_up\", \"Scroll Up\", show=False),\n    Binding(\n        \"down\", \"scroll_down\", \"Scroll Down\", show=False\n    ),\n    Binding(\"left\", \"scroll_left\", \"Scroll Up\", show=False),\n    Binding(\n        \"right\", \"scroll_right\", \"Scroll Right\", show=False\n    ),\n    Binding(\n        \"home\", \"scroll_home\", \"Scroll Home\", show=False\n    ),\n    Binding(\"end\", \"scroll_end\", \"Scroll End\", show=False),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n]\n

    Keyboard bindings for scrollable containers.

    Key(s) Description up Scroll up, if vertical scrolling is available. down Scroll down, if vertical scrolling is available. left Scroll left, if horizontal scrolling is available. right Scroll right, if horizontal scrolling is available. home Scroll to the home position, if scrolling is available. end Scroll to the end position, if scrolling is available. pageup Scroll up one page, if vertical scrolling is available. pagedown Scroll down one page, if vertical scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical class","text":"

    Bases: Widget

    A container with vertical layout and no scrollbars.

    "},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll class","text":"

    Bases: ScrollableContainer

    A container with vertical layout and an automatic scrollbar on the Y axis.

    "},{"location":"api/containers/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"api/containers/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"api/content_switcher/","title":"Content switcher","text":""},{"location":"api/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"api/coordinate/","title":"Coordinate","text":"

    A class to store a coordinate, used by the DataTable.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate class","text":"

    Bases: NamedTuple

    An object representing a row/column coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.column","title":"column instance-attribute","text":"
    column: int\n

    The column of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
    row: int\n

    The row of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down method","text":"
    def down(self):\n

    Get the coordinate below.

    Returns Type Description Coordinate

    The coordinate below.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left method","text":"
    def left(self):\n

    Get the coordinate to the left.

    Returns Type Description Coordinate

    The coordinate to the left.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.right","title":"right method","text":"
    def right(self):\n

    Get the coordinate to the right.

    Returns Type Description Coordinate

    The coordinate to the right.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.up","title":"up method","text":"
    def up(self):\n

    Get the coordinate above.

    Returns Type Description Coordinate

    The coordinate above.

    "},{"location":"api/dom_node/","title":"Dom node","text":"

    A DOMNode is a base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

    "},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
    WalkMethod: TypeAlias = Literal['depth', 'breadth']\n

    Valid walking methods for the DOMNode.walk_children method.

    "},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier class","text":"

    Bases: Exception

    Exception raised if you supply a id attribute or class name in the wrong format.

    "},{"location":"api/dom_node/#textual.dom.DOMError","title":"DOMError class","text":"

    Bases: Exception

    Base exception class for errors relating to the DOM.

    "},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode class","text":"
    def __init__(self, *, name=None, id=None, classes=None):\n

    Bases: MessagePump

    The base class for object that can be in the Textual DOM (App and Widget)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
    SCOPED_CSS: bool = True\n

    Should default css be limited to the widget type?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
    ancestors: list[DOMNode]\n

    A list of ancestor nodes found by tracing a path all the way back to App.

    Returns Type Description list[DOMNode]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors_with_self","title":"ancestors_with_self property","text":"
    ancestors_with_self: list[DOMNode]\n

    A list of ancestor nodes found by tracing a path all the way back to App.

    Note

    This is inclusive of self.

    Returns Type Description list[DOMNode]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.auto_refresh","title":"auto_refresh writable property","text":"
    auto_refresh: float | None\n

    Number of seconds between automatic refresh, or None for no automatic refresh.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.background_colors","title":"background_colors property","text":"
    background_colors: tuple[Color, Color]\n

    The background color and the color of the parent's background.

    Returns Type Description tuple[Color, Color]

    (<background color>, <color>)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.children","title":"children property","text":"
    children: Sequence['Widget']\n

    A view on to the children.

    Returns Type Description Sequence['Widget']

    The node's children.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.classes","title":"classes class-attribute instance-attribute","text":"
    classes = _ClassesDescriptor()\n

    CSS class names for this node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.colors","title":"colors property","text":"
    colors: tuple[Color, Color, Color, Color]\n

    The widget's background and foreground colors, and the parent's background and foreground colors.

    Returns Type Description tuple[Color, Color, Color, Color]

    (<parent background>, <parent color>, <background>, <color>)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier","title":"css_identifier property","text":"
    css_identifier: str\n

    A CSS selector that identifies this DOM node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier_styled","title":"css_identifier_styled property","text":"
    css_identifier_styled: Text\n

    A syntax highlighted CSS identifier.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_path_nodes","title":"css_path_nodes property","text":"
    css_path_nodes: list[DOMNode]\n

    A list of nodes from the App to this node, forming a \"path\".

    Returns Type Description list[DOMNode]

    A list of nodes, where the first item is the App, and the last is this node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.css_tree","title":"css_tree property","text":"
    css_tree: Tree\n

    A Rich tree to display the DOM, annotated with the node's CSS.

    Log this to visualize your app in the textual console.

    Example
    self.log(self.css_tree)\n
    Returns Type Description Tree

    A Tree renderable.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.display","title":"display writable property","text":"
    display: bool\n

    Should the DOM node be displayed?

    May be set to a boolean to show or hide the node, or to any valid value for the display rule.

    Example
    my_widget.display = False  # Hide my_widget\n
    "},{"location":"api/dom_node/#textual.dom.DOMNode.displayed_children","title":"displayed_children property","text":"
    displayed_children: list[Widget]\n

    The child nodes which will be displayed.

    Returns Type Description list[Widget]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.id","title":"id writable property","text":"
    id: str | None\n

    The ID of this node, or None if the node has no ID.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.is_modal","title":"is_modal property","text":"
    is_modal: bool\n

    Is the node a modal?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
    name: str | None\n

    The name of the node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
    parent: DOMNode | None\n

    The parent node.

    All nodes have parent once added to the DOM, with the exception of the App which is the root node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.pseudo_classes","title":"pseudo_classes property","text":"
    pseudo_classes: frozenset[str]\n

    A (frozen) set of all pseudo classes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
    rich_style: Style\n

    Get a Rich Style object for this DOMNode.

    Returns Type Description Style

    A Rich style.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.screen","title":"screen property","text":"
    screen: 'Screen[object]'\n

    The screen containing this node.

    Returns Type Description 'Screen[object]'

    A screen object.

    Raises Type Description NoScreen

    If this node isn't mounted (and has no screen).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.text_style","title":"text_style property","text":"
    text_style: Style\n

    Get the text style object.

    A widget's style is influenced by its parent. for instance if a parent is bold, then the child will also be bold.

    Returns Type Description Style

    A Rich Style.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.tree","title":"tree property","text":"
    tree: Tree\n

    A Rich tree to display the DOM.

    Log this to visualize your app in the textual console.

    Example
    self.log(self.tree)\n
    Returns Type Description Tree

    A Tree renderable.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.visible","title":"visible writable property","text":"
    visible: bool\n

    Is this widget visible in the DOM?

    If a widget hasn't had its visibility set explicitly, then it inherits it from its DOM ancestors.

    This may be set explicitly to override inherited values. The valid values include the valid values for the visibility rule and the booleans True or False, to set the widget to be visible or invisible, respectively.

    When a node is invisible, Textual will reserve space for it, but won't display anything.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.workers","title":"workers property","text":"
    workers: WorkerManager\n

    The app's worker manager. Shortcut for self.app.workers.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class method","text":"
    def add_class(self, *class_names, update=True):\n

    Add class names to this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to add.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles method","text":"
    def get_component_styles(self, name):\n

    Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

    Parameters Parameter Default Description name str required

    Name of the component.

    Raises Type Description KeyError

    If the component class doesn't exist.

    Returns Type Description RenderStyles

    A Styles object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes method","text":"
    def get_pseudo_classes(self):\n

    Get any pseudo classes applicable to this Node, e.g. hover, focus.

    Returns Type Description Iterable[str]

    Iterable of strings, such as a generator.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_class","title":"has_class method","text":"
    def has_class(self, *class_names):\n

    Check if the Node has all the given class names.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to check.

    Returns Type Description bool

    True if the node has all the given class names, otherwise False.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class method","text":"
    def has_pseudo_class(self, class_name):\n

    Check the node has the given pseudo class.

    Parameters Parameter Default Description class_name str required

    The pseudo class to check for.

    Returns Type Description bool

    True if the DOM node has the pseudo class, False if not.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_classes","title":"has_pseudo_classes method","text":"
    def has_pseudo_classes(self, class_names):\n

    Check the node has all the given pseudo classes.

    Parameters Parameter Default Description class_names set[str] required

    Set of class names to check for.

    Returns Type Description bool

    True if all pseudo class names are present.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update method","text":"
    def notify_style_update(self):\n

    Called after styles are updated.

    Implement this in a subclass if you want to clear any cached data when the CSS is reloaded.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query","title":"query method","text":"
    def query(self, selector=None):\n

    Get a DOM query matching a selector.

    Parameters Parameter Default Description selector str | type[QueryType] | None None

    A CSS selector or None for all nodes.

    Returns Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one method","text":"
    def query_one(self, selector, expect_type=None):\n

    Get a single Widget matching the given selector or selector type.

    Parameters Parameter Default Description selector str | type[QueryType] required

    A selector.

    expect_type type[QueryType] | None None

    Require the object be of the supplied type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    TooManyMatches

    If there is more than one matching node in the query.

    Returns Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class method","text":"
    def remove_class(self, *class_names, update=True):\n

    Remove class names from this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to remove.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles method","text":"
    def reset_styles(self):\n

    Reset styles back to their initial state.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker method","text":"
    def run_worker(\n    self,\n    work,\n    name=\"\",\n    group=\"default\",\n    description=\"\",\n    exit_on_error=True,\n    start=True,\n    exclusive=False,\n    thread=False,\n):\n

    Run work in a worker.

    A worker runs a function, coroutine, or awaitable, in the background as an async task or as a thread.

    Parameters Parameter Default Description work WorkType[ResultType] required

    A function, async function, or an awaitable object to run in a worker.

    name str | None ''

    A short string to identify the worker (in logs and debugging).

    group str 'default'

    A short string to identify a group of workers.

    description str ''

    A longer string to store longer information on the worker.

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    start bool True

    Start the worker immediately.

    exclusive bool False

    Cancel all workers in the same group.

    thread bool False

    Mark the worker as a thread worker.

    Returns Type Description Worker[ResultType]

    New Worker instance.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class method","text":"
    def set_class(self, add, *class_names, update=True):\n

    Add or remove class(es) based on a condition.

    Parameters Parameter Default Description add bool required

    Add the classes if True, otherwise remove them.

    update bool True

    Also update styles.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes method","text":"
    def set_classes(self, classes):\n

    Replace all classes.

    Parameters Parameter Default Description classes str | Iterable[str] required

    A string containing space separated classes, or an iterable of class names.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles method","text":"
    def set_styles(self, css=None, **update_styles):\n

    Set custom styles on this object.

    Parameters Parameter Default Description css str | None None

    Styles in CSS format.

    update_styles Any {}

    Keyword arguments map style names onto style values.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class method","text":"
    def toggle_class(self, *class_names):\n

    Toggle class names on this Node.

    Parameters Parameter Default Description *class_names str ()

    CSS class names to toggle.

    Returns Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children method","text":"
    def walk_children(\n    self,\n    filter_type=None,\n    *,\n    with_self=False,\n    method=\"depth\",\n    reverse=False\n):\n

    Walk the subtree rooted at this node, and return every descendant encountered in a list.

    Parameters Parameter Default Description filter_type type[WalkType] | None None

    Filter only this type, or None for no filter.

    with_self bool False

    Also yield self in addition to descendants.

    method WalkMethod 'depth'

    One of \"depth\" or \"breadth\".

    reverse bool False

    Reverse the order (bottom up).

    Returns Type Description list[DOMNode] | list[WalkType]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch method","text":"
    def watch(self, obj, attribute_name, callback, init=True):\n

    Watches for modifications to reactive attributes on another object.

    Example

    Here's how you could detect when the app changes from dark to light mode (and vice versa).

    def on_dark_change(old_value:bool, new_value:bool):\n    # Called when app.dark changes.\n    print(\"App.dark when from {old_value} to {new_value}\")\n\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n
    Parameters Parameter Default Description obj DOMNode required

    Object containing attribute to watch.

    attribute_name str required

    Attribute to watch.

    callback WatchCallbackType required

    A callback to run when attribute changes.

    init bool True

    Check watchers on first call.

    "},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen class","text":"

    Bases: DOMError

    Raised when the node has no associated screen.

    "},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers function","text":"
    def check_identifiers(description, *names):\n

    Validate identifier and raise an error if it fails.

    Parameters Parameter Default Description description str required

    Description of where identifier is used for error message.

    *names str ()

    Identifiers to check.

    "},{"location":"api/errors/","title":"Errors","text":"

    General exception classes.

    "},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers class","text":"

    Bases: TextualError

    More than one handler for a single key press.

    For example, if the handlers key_ctrl_i and key_tab were defined on the same widget, then this error would be raised.

    "},{"location":"api/errors/#textual.errors.NoWidget","title":"NoWidget class","text":"

    Bases: TextualError

    Specified widget was not found.

    "},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError class","text":"

    Bases: TextualError

    An object could not be rendered.

    "},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError class","text":"

    Bases: Exception

    Base class for Textual errors.

    "},{"location":"api/events/","title":"Events","text":"

    Builtin events sent by Textual.

    Events may be marked as \"Bubbles\" and \"Verbose\". See the events guide for an explanation of bubbling. Verbose events are excluded from the textual console, unless you explicitly request them with the -v switch as follows:

    textual console -v\n
    "},{"location":"api/events/#textual.events.AppBlur","title":"AppBlur class","text":"

    Bases: Event

    Sent when the app loses focus.

    Used by textual-web.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.AppFocus","title":"AppFocus class","text":"

    Bases: Event

    Sent when the app has focus.

    Used by textual-web.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Blur","title":"Blur class","text":"

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Click","title":"Click class","text":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Compose","title":"Compose class","text":"

    Bases: Event

    Sent to a widget to request it to compose and mount children.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur class","text":"

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
    control: Widget\n

    The widget that was blurred (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was blurred.

    "},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus class","text":"

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
    control: Widget\n

    The widget that was focused (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was focused.

    "},{"location":"api/events/#textual.events.Enter","title":"Enter class","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Event","title":"Event class","text":"

    Bases: Message

    The base class for all events.

    "},{"location":"api/events/#textual.events.Focus","title":"Focus class","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Hide","title":"Hide class","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

    "},{"location":"api/events/#textual.events.Idle","title":"Idle class","text":"

    Bases: Event

    Sent when there are no more items in the message queue.

    This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.InputEvent","title":"InputEvent class","text":"

    Bases: Event

    Base class for input events.

    "},{"location":"api/events/#textual.events.Key","title":"Key class","text":"
    def __init__(self, key, character):\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description key str required

    The key that was pressed.

    character str | None required

    A printable character or None if it is not printable.

    Attributes Name Type Description aliases list[str]

    The aliases for the key, including the key itself.

    "},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable: bool\n

    Check if the key is printable (produces a unicode character).

    Returns Type Description bool

    True if the key is printable.

    "},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
    name: str\n

    Name of a key suitable for use as a Python identifier.

    "},{"location":"api/events/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
    name_aliases: list[str]\n

    The corresponding name for every alias in aliases list.

    "},{"location":"api/events/#textual.events.Leave","title":"Leave class","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Load","title":"Load class","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Mount","title":"Mount class","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Sent when the mouse has been captured.

    • Bubbles
    • Verbose

    When a mouse has been captured, all further mouse events will be sent to the capturing widget.

    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when captured.

    "},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown class","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent class","text":"
    def __init__(\n    self,\n    x,\n    y,\n    delta_x,\n    delta_y,\n    button,\n    shift,\n    meta,\n    ctrl,\n    screen_x=None,\n    screen_y=None,\n    style=None,\n):\n

    Bases: InputEvent

    Sent in response to a mouse event.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description x int required

    The relative x coordinate.

    y int required

    The relative y coordinate.

    delta_x int required

    Change in x since the last message.

    delta_y int required

    Change in y since the last message.

    button int required

    Indexed of the pressed button.

    shift bool required

    True if the shift key is pressed.

    meta bool required

    True if the meta key is pressed.

    ctrl bool required

    True if the ctrl key is pressed.

    screen_x int | None None

    The absolute x coordinate.

    screen_y int | None None

    The absolute y coordinate.

    style Style | None None

    The Rich Style under the mouse cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
    delta: Offset\n

    Mouse coordinate delta (change since last event).

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
    offset: Offset\n

    The mouse coordinate as an offset.

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_offset","title":"screen_offset property","text":"
    screen_offset: Offset\n

    Mouse coordinate relative to the screen.

    Returns Type Description Offset

    Mouse coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.style","title":"style writable property","text":"
    style: Style\n

    The (Rich) Style under the cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset method","text":"
    def get_content_offset(self, widget):\n

    Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

    Parameters Parameter Default Description widget Widget required

    Widget receiving the event.

    Returns Type Description Offset | None

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture method","text":"
    def get_content_offset_capture(self, widget):\n

    Get offset from a widget's content area.

    This method works even if the offset is outside the widget content region.

    Parameters Parameter Default Description widget Widget required

    Widget receiving the event.

    Returns Type Description Offset

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove class","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when released.

    "},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp class","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Paste","title":"Paste class","text":"
    def __init__(self, text):\n

    Bases: Event

    Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    The text that has been pasted.

    "},{"location":"api/events/#textual.events.Print","title":"Print class","text":"
    def __init__(self, text, stderr=False):\n

    Bases: Event

    Sent to a widget that is capturing prints.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    Text that was printed.

    stderr bool False

    True if the print was to stderr, or False for stdout.

    "},{"location":"api/events/#textual.events.Ready","title":"Ready class","text":"

    Bases: Event

    Sent to the app when the DOM is ready.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Resize","title":"Resize class","text":"
    def __init__(self, size, virtual_size, container_size=None):\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description size Size required

    The new size of the Widget.

    virtual_size Size required

    The virtual size (scrollable size) of the Widget.

    container_size Size | None None

    The size of the Widget's container widget.

    "},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume class","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend class","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Show","title":"Show class","text":"

    Bases: Event

    Sent when a widget has become visible.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Timer","title":"Timer class","text":"
    def __init__(self, timer, time, count=0, callback=None):\n

    Bases: Event

    Sent in response to a timer.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Unmount","title":"Unmount class","text":"

    Bases: Event

    Sent when a widget is unmounted and may not longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/filter/","title":"Filter","text":"

    Filter classes.

    Note

    Filters are used internally, and not recommended for use by Textual app developers.

    Filters are used internally to process terminal output after it has been rendered. Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set.

    In the future, this system will be used to implement accessibility features.

    "},{"location":"api/filter/#textual.filter.NO_DIM","title":"NO_DIM module-attribute","text":"
    NO_DIM = Style(dim=False)\n

    A Style to set dim to False.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor","title":"ANSIToTruecolor class","text":"
    def __init__(self, terminal_theme):\n

    Bases: LineFilter

    Convert ANSI colors to their truecolor equivalents.

    Parameters Parameter Default Description terminal_theme TerminalTheme required

    A rich terminal theme.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
    def truecolor_style(self, style):\n

    Replace system colors with truecolor equivalent.

    Parameters Parameter Default Description style Style required

    Style to apply truecolor filter to.

    Returns Type Description Style

    New style.

    "},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter class","text":"
    def __init__(self, dim_factor=0.5):\n

    Bases: LineFilter

    Replace dim attributes with modified colors.

    Parameters Parameter Default Description dim_factor float 0.5

    The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

    "},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter class","text":"

    Bases: ABC

    Base class for a line filter.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome class","text":"

    Bases: LineFilter

    Convert all colors to monochrome.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply method","text":"
    def apply(self, segments, background):\n

    Transform a list of segments.

    Parameters Parameter Default Description segments list[Segment] required

    A list of segments.

    background Color required

    The background color.

    Returns Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
    def dim_color(background, color, factor):\n

    Dim a color by blending towards the background

    Parameters Parameter Default Description background RichColor required

    background color.

    color RichColor required

    Foreground color.

    factor float required

    Blend factor

    Returns Type Description RichColor

    New dimmer color.

    "},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
    def dim_style(style, background, factor):\n

    Replace dim attribute with a dim color.

    Parameters Parameter Default Description style Style required

    Style to dim.

    factor float required

    Blend factor.

    Returns Type Description Style

    New dimmed style.

    "},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
    def monochrome_style(style):\n

    Convert colors in a style to monochrome.

    Parameters Parameter Default Description style Style required

    A Rich Style.

    Returns Type Description Style

    A new Rich style.

    "},{"location":"api/fuzzy_matcher/","title":"Fuzzy matcher","text":"

    Fuzzy matcher.

    This class is used by the command palette to match search terms.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher class","text":"
    def __init__(\n    self, query, *, match_style=None, case_sensitive=False\n):\n

    A fuzzy matcher.

    Parameters Parameter Default Description query str required

    A query as typed in by the user.

    match_style Style | None None

    The style to use to highlight matched portions of a string.

    case_sensitive bool False

    Should matching be case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive: bool\n

    Is this matcher case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_style: Style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query","title":"query property","text":"
    query: str\n

    The query string to look for.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern: str\n

    The regular expression pattern built from the query.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
    def highlight(self, candidate):\n

    Highlight the candidate with the fuzzy match.

    Parameters Parameter Default Description candidate str required

    The candidate string to match against the query.

    Returns Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match method","text":"
    def match(self, candidate):\n

    Match the candidate against the query.

    Parameters Parameter Default Description candidate str required

    Candidate string to match against the query.

    Returns Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/geometry/","title":"Geometry","text":"

    Functions and classes to manage terminal geometry (anything involving coordinates or dimensions).

    "},{"location":"api/geometry/#textual.geometry.NULL_OFFSET","title":"NULL_OFFSET module-attribute","text":"
    NULL_OFFSET: Final = Offset(0, 0)\n

    An offset constant for (0, 0).

    "},{"location":"api/geometry/#textual.geometry.NULL_REGION","title":"NULL_REGION module-attribute","text":"
    NULL_REGION: Final = Region(0, 0, 0, 0)\n

    A Region constant for a null region (at the origin, with both width and height set to zero).

    "},{"location":"api/geometry/#textual.geometry.NULL_SIZE","title":"NULL_SIZE module-attribute","text":"
    NULL_SIZE: Final = Size(0, 0)\n

    A Size constant for a null size (with zero area).

    "},{"location":"api/geometry/#textual.geometry.NULL_SPACING","title":"NULL_SPACING module-attribute","text":"
    NULL_SPACING: Final = Spacing(0, 0, 0, 0)\n

    A Spacing constant for no space.

    "},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
    SpacingDimensions: TypeAlias = Union[\n    int,\n    Tuple[int],\n    Tuple[int, int],\n    Tuple[int, int, int, int],\n]\n

    The valid ways in which you can specify spacing.

    "},{"location":"api/geometry/#textual.geometry.Offset","title":"Offset class","text":"

    Bases: NamedTuple

    A cell offset defined by x and y coordinates.

    Offsets are typically relative to the top left of the terminal or other container.

    Textual prefers the names x and y, but you could consider x to be the column and y to be the row.

    Offsets support addition, subtraction, multiplication, and negation.

    Example
    >>> from textual.geometry import Offset\n>>> offset = Offset(3, 2)\n>>> offset\nOffset(x=3, y=2)\n>>> offset += Offset(10, 0)\n>>> offset\nOffset(x=13, y=2)\n>>> -offset\nOffset(x=-13, y=-2)\n
    "},{"location":"api/geometry/#textual.geometry.Offset.clamped","title":"clamped property","text":"
    clamped: Offset\n

    This offset with x and y restricted to values above zero.

    "},{"location":"api/geometry/#textual.geometry.Offset.is_origin","title":"is_origin property","text":"
    is_origin: bool\n

    Is the offset at (0, 0)?

    "},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x class-attribute instance-attribute","text":"
    x: int = 0\n

    Offset in the x-axis (horizontal)

    "},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y class-attribute instance-attribute","text":"
    y: int = 0\n

    Offset in the y-axis (vertical)

    "},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend method","text":"
    def blend(self, destination, factor):\n

    Calculate a new offset on a line between this offset and a destination offset.

    Parameters Parameter Default Description destination Offset required

    Point where factor would be 1.0.

    factor float required

    A value between 0 and 1.0.

    Returns Type Description Offset

    A new point on a line between self and destination.

    "},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to method","text":"
    def get_distance_to(self, other):\n

    Get the distance to another offset.

    Parameters Parameter Default Description other Offset required

    An offset.

    Returns Type Description float

    Distance to other offset.

    "},{"location":"api/geometry/#textual.geometry.Region","title":"Region class","text":"

    Bases: NamedTuple

    Defines a rectangular region.

    A Region consists of a coordinate (x and y) and dimensions (width and height).

      (x, y)\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u25b2\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 height\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u25bc\n    \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500 width \u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n
    Example
    >>> from textual.geometry import Region\n>>> region = Region(4, 5, 20, 10)\n>>> region\nRegion(x=4, y=5, width=20, height=10)\n>>> region.area\n200\n>>> region.size\nSize(width=20, height=10)\n>>> region.offset\nOffset(x=4, y=5)\n>>> region.contains(1, 2)\nFalse\n>>> region.contains(10, 8)\nTrue\n
    "},{"location":"api/geometry/#textual.geometry.Region.area","title":"area property","text":"
    area: int\n

    The area under the region.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
    bottom: int\n

    Maximum Y value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
    bottom_left: Offset\n

    Bottom left offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_right","title":"bottom_right property","text":"
    bottom_right: Offset\n

    Bottom right offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.center","title":"center property","text":"
    center: tuple[float, float]\n

    The center of the region.

    Note, that this does not return an Offset, because the center may not be an integer coordinate.

    Returns Type Description tuple[float, float]

    Tuple of floats.

    "},{"location":"api/geometry/#textual.geometry.Region.column_range","title":"column_range property","text":"
    column_range: range\n

    A range object for X coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
    column_span: tuple[int, int]\n

    A pair of integers for the start and end columns (x coordinates) in this region.

    The end value is exclusive.

    "},{"location":"api/geometry/#textual.geometry.Region.corners","title":"corners property","text":"
    corners: tuple[int, int, int, int]\n

    The top left and bottom right coordinates as a tuple of four integers.

    "},{"location":"api/geometry/#textual.geometry.Region.height","title":"height class-attribute instance-attribute","text":"
    height: int = 0\n

    The height of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
    line_range: range\n

    A range object for Y coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
    line_span: tuple[int, int]\n

    A pair of integers for the start and end lines (y coordinates) in this region.

    The end value is exclusive.

    "},{"location":"api/geometry/#textual.geometry.Region.offset","title":"offset property","text":"
    offset: Offset\n

    The top left corner of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.reset_offset","title":"reset_offset property","text":"
    reset_offset: Region\n

    An region of the same size at (0, 0).

    Returns Type Description Region

    A region at the origin.

    "},{"location":"api/geometry/#textual.geometry.Region.right","title":"right property","text":"
    right: int\n

    Maximum X value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
    size: Size\n

    Get the size of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
    top_right: Offset\n

    Top right offset of the region.

    Returns Type Description Offset

    An offset.

    "},{"location":"api/geometry/#textual.geometry.Region.width","title":"width class-attribute instance-attribute","text":"
    width: int = 0\n

    The width of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.x","title":"x class-attribute instance-attribute","text":"
    x: int = 0\n

    Offset in the x-axis (horizontal).

    "},{"location":"api/geometry/#textual.geometry.Region.y","title":"y class-attribute instance-attribute","text":"
    y: int = 0\n

    Offset in the y-axis (vertical).

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset method","text":"
    def at_offset(self, offset):\n

    Get a new Region with the same size at a given offset.

    Parameters Parameter Default Description offset tuple[int, int] required

    An offset.

    Returns Type Description Region

    New Region with adjusted offset.

    "},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip method","text":"
    def clip(self, width, height):\n

    Clip this region to fit within width, height.

    Parameters Parameter Default Description width int required

    Width of bounds.

    height int required

    Height of bounds.

    Returns Type Description Region

    Clipped region.

    "},{"location":"api/geometry/#textual.geometry.Region.clip_size","title":"clip_size method","text":"
    def clip_size(self, size):\n

    Clip the size to fit within minimum values.

    Parameters Parameter Default Description size tuple[int, int] required

    Maximum width and height.

    Returns Type Description Region

    No region, not bigger than size.

    "},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains method","text":"
    def contains(self, x, y):\n

    Check if a point is in the region.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point method","text":"
    def contains_point(self, point):\n

    Check if a point is in the region.

    Parameters Parameter Default Description point tuple[int, int] required

    A tuple of x and y coordinates.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
    def contains_region(self, other):\n

    Check if a region is entirely contained within this region.

    Parameters Parameter Default Description other Region required

    A region.

    Returns Type Description bool

    True if the other region fits perfectly within this region.

    "},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size method","text":"
    def crop_size(self, size):\n

    Get a region with the same offset, with a size no larger than size.

    Parameters Parameter Default Description size tuple[int, int] required

    Maximum width and height (WIDTH, HEIGHT).

    Returns Type Description Region

    New region that could fit within size.

    "},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand method","text":"
    def expand(self, size):\n

    Increase the size of the region by adding a border.

    Parameters Parameter Default Description size tuple[int, int] required

    Additional width and height.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
    def from_corners(cls, x1, y1, x2, y2):\n

    Construct a Region form the top left and bottom right corners.

    Parameters Parameter Default Description x1 int required

    Top left x.

    y1 int required

    Top left y.

    x2 int required

    Bottom right x.

    y2 int required

    Bottom right y.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
    def from_offset(cls, offset, size):\n

    Create a region from offset and size.

    Parameters Parameter Default Description offset tuple[int, int] required

    Offset (top left point).

    size tuple[int, int] required

    Dimensions of region.

    Returns Type Description Region

    A region instance.

    "},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
    def from_union(cls, regions):\n

    Create a Region from the union of other regions.

    Parameters Parameter Default Description regions Collection[Region] required

    One or more regions.

    Returns Type Description Region

    A Region that encloses all other regions.

    "},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
    def get_scroll_to_visible(\n    cls, window_region, region, *, top=False\n):\n

    Calculate the smallest offset required to translate a window so that it contains another region.

    This method is used to calculate the required offset to scroll something in to view.

    Parameters Parameter Default Description window_region Region required

    The window region.

    region Region required

    The region to move inside the window.

    top bool False

    Get offset to top of window.

    Returns Type Description Offset

    An offset required to add to region to move it inside window_region.

    "},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
    def grow(self, margin):\n

    Grow a region by adding spacing.

    Parameters Parameter Default Description margin tuple[int, int, int, int] required

    Grow space by (<top>, <right>, <bottom>, <left>).

    Returns Type Description Region

    New region.

    "},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect method","text":"
    def inflect(self, x_axis=+1, y_axis=+1, margin=None):\n

    Inflect a region around one or both axis.

    The x_axis and y_axis parameters define which direction to move the region. A positive value will move the region right or down, a negative value will move the region left or up. A value of 0 will leave that axis unmodified.

    \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557    \u2502\n\u2551          \u2551\n\u2551   Self   \u2551    \u2502\n\u2551          \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d    \u2502\n\n\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u253c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\n\n                \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502          \u2502\n                \u2502    \u2502  Result  \u2502\n                     \u2502          \u2502\n                \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description x_axis int +1

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    y_axis int +1

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    margin Spacing | None None

    Additional margin.

    Returns Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
    def intersection(self, region):\n

    Get the overlapping portion of the two regions.

    Parameters Parameter Default Description region Region required

    A region that overlaps this region.

    Returns Type Description Region

    A new region that covers when the two regions overlap.

    "},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
    def overlaps(self, other):\n

    Check if another region overlaps this region.

    Parameters Parameter Default Description other Region required

    A Region.

    Returns Type Description bool

    True if other region shares any cells with this region.

    "},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
    def shrink(self, margin):\n

    Shrink a region by subtracting spacing.

    Parameters Parameter Default Description margin tuple[int, int, int, int] required

    Shrink space by (<top>, <right>, <bottom>, <left>).

    Returns Type Description Region

    The new, smaller region.

    "},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
    def split(self, cut_x, cut_y):\n

    Split a region in to 4 from given x and y offsets (cuts).

               cut_x \u2193\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502        \u2502 \u2502   \u2502\n        \u2502    0   \u2502 \u2502 1 \u2502\n        \u2502        \u2502 \u2502   \u2502\ncut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502    2   \u2502 \u2502 3 \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut_x int required

    Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

    cut_y int required

    Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

    Returns Type Description tuple[Region, Region, Region, Region]

    Four new regions which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
    def split_horizontal(self, cut):\n

    Split a region in to two, from a given y offset.

                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    0    \u2502\n            \u2502         \u2502\n    cut \u2192   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    1    \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut int required

    An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

    Returns Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
    def split_vertical(self, cut):\n

    Split a region in to two, from a given x offset.

             cut \u2193\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2510\n    \u2502    0   \u2502\u2502 1 \u2502\n    \u2502        \u2502\u2502   \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description cut int required

    An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

    Returns Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
    def translate(self, offset):\n

    Move the offset of the Region.

    Parameters Parameter Default Description offset tuple[int, int] required

    Offset to add to region.

    Returns Type Description Region

    A new region shifted by (x, y)

    "},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside method","text":"
    def translate_inside(self, container, x_axis=True, y_axis=True):\n

    Translate this region, so it fits within a container.

    This will ensure that there is as little overlap as possible. The top left of the returned region is guaranteed to be within the container.

    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502    container     \u2502         \u2502    container     \u2502\n\u2502                  \u2502         \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502                  \u2502   \u2500\u2500\u25b6   \u2502    \u2502    return   \u2502\n\u2502       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2510      \u2502    \u2502             \u2502\n\u2502       \u2502    self     \u2502      \u2502    \u2502             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524             \u2502      \u2514\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502             \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
    Parameters Parameter Default Description container Region required

    A container region.

    x_axis bool True

    Allow translation of X axis.

    y_axis bool True

    Allow translation of Y axis.

    Returns Type Description Region

    A new region with same dimensions that fits with inside container.

    "},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
    def union(self, region):\n

    Get the smallest region that contains both regions.

    Parameters Parameter Default Description region Region required

    Another region.

    Returns Type Description Region

    An optimally sized region to cover both regions.

    "},{"location":"api/geometry/#textual.geometry.Size","title":"Size class","text":"

    Bases: NamedTuple

    The dimensions (width and height) of a rectangular region.

    Example
    >>> from textual.geometry import Size\n>>> size = Size(2, 3)\n>>> size\nSize(width=2, height=3)\n>>> size.area\n6\n>>> size + Size(10, 20)\nSize(width=12, height=23)\n
    "},{"location":"api/geometry/#textual.geometry.Size.area","title":"area property","text":"
    area: int\n

    The area occupied by a region of this size.

    "},{"location":"api/geometry/#textual.geometry.Size.height","title":"height class-attribute instance-attribute","text":"
    height: int = 0\n

    The height in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
    line_range: range\n

    A range object that covers values between 0 and height.

    "},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
    region: Region\n

    A region of the same size, at the origin.

    "},{"location":"api/geometry/#textual.geometry.Size.width","title":"width class-attribute instance-attribute","text":"
    width: int = 0\n

    The width in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains method","text":"
    def contains(self, x, y):\n

    Check if a point is in area defined by the size.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point method","text":"
    def contains_point(self, point):\n

    Check if a point is in the area defined by the size.

    Parameters Parameter Default Description point tuple[int, int] required

    A tuple of x and y coordinates.

    Returns Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing class","text":"

    Bases: NamedTuple

    The spacing around a renderable, such as padding and border.

    Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.

    \u250c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25b2\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2510\n               \u2502 top\n\u2502        \u250f\u2501\u2501\u2501\u2501\u2501\u25bc\u2501\u2501\u2501\u2501\u2501\u2501\u2513         \u2502\n \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2503            \u2503\u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n\u2502  left  \u2503            \u2503 right   \u2502\n         \u2503            \u2503\n\u2502        \u2517\u2501\u2501\u2501\u2501\u2501\u25b2\u2501\u2501\u2501\u2501\u2501\u2501\u251b         \u2502\n               \u2502 bottom\n\u2514 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25bc\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2518\n
    Example
    >>> from textual.geometry import Region, Spacing\n>>> region = Region(2, 3, 20, 10)\n>>> spacing = Spacing(1, 2, 3, 4)\n>>> region.grow(spacing)\nRegion(x=-2, y=2, width=26, height=14)\n>>> region.shrink(spacing)\nRegion(x=6, y=4, width=14, height=6)\n>>> spacing.css\n'1 2 3 4'\n
    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom","title":"bottom class-attribute instance-attribute","text":"
    bottom: int = 0\n

    Space from the bottom of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
    bottom_right: tuple[int, int]\n

    A pair of integers for the right, and bottom space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
    css: str\n

    A string containing the spacing in CSS format.

    For example: \"1\" or \"2 4\" or \"4 2 8 2\".

    "},{"location":"api/geometry/#textual.geometry.Spacing.height","title":"height property","text":"
    height: int\n

    Total space in the y axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left class-attribute instance-attribute","text":"
    left: int = 0\n

    Space from the left of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right class-attribute instance-attribute","text":"
    right: int = 0\n

    Space from the right of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top class-attribute instance-attribute","text":"
    top: int = 0\n

    Space from the top of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
    top_left: tuple[int, int]\n

    A pair of integers for the left, and top space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
    totals: tuple[int, int]\n

    A pair of integers for the total horizontal and vertical space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
    width: int\n

    Total space in the x axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
    def all(cls, amount):\n

    Construct a Spacing with a given amount of spacing on all edges.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to all edges.

    Returns Type Description Spacing

    Spacing(amount, amount, amount, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum method","text":"
    def grow_maximum(self, other):\n

    Grow spacing with a maximum.

    Parameters Parameter Default Description other Spacing required

    Spacing object.

    Returns Type Description Spacing

    New spacing where the values are maximum of the two values.

    "},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
    def horizontal(cls, amount):\n

    Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to horizontal edges.

    Returns Type Description Spacing

    Spacing(0, amount, 0, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
    def unpack(cls, pad):\n

    Unpack padding specified in CSS style.

    Parameters Parameter Default Description pad SpacingDimensions required

    An integer, or tuple of 1, 2, or 4 integers.

    Raises Type Description ValueError

    If pad is an invalid value.

    Returns Type Description Spacing

    New Spacing object.

    "},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
    def vertical(cls, amount):\n

    Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

    Parameters Parameter Default Description amount int required

    The magnitude of spacing to apply to vertical edges.

    Returns Type Description Spacing

    Spacing(amount, 0, amount, 0)

    "},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp function","text":"
    def clamp(value, minimum, maximum):\n

    Adjust a value so it is not less than a minimum and not greater than a maximum value.

    Parameters Parameter Default Description value T required

    A value.

    minimum T required

    Minimum value.

    maximum T required

    Maximum value.

    Returns Type Description T

    New value that is not less than the minimum or greater than the maximum.

    "},{"location":"api/lazy/","title":"Lazy","text":"

    Tools for lazy loading widgets.

    "},{"location":"api/lazy/#textual.lazy.Lazy","title":"Lazy class","text":"
    def __init__(self, widget):\n

    Bases: Widget

    Wraps a widget so that it is mounted lazily.

    Lazy widgets are mounted after the first refresh. This can be used to display some parts of the UI very quickly, followed by the lazy widgets. Technically, this won't make anything faster, but it reduces the time the user sees a blank screen and will make apps feel more responsive.

    Making a widget lazy is beneficial for widgets which start out invisible, such as tab panes.

    Note that since lazy widgets aren't mounted immediately (by definition), they will not appear in queries for a brief interval until they are mounted. Your code should take this in to account.

    Example
    def compose(self) -> ComposeResult:\n    yield Footer()\n    with ColorTabs(\"Theme Colors\", \"Named Colors\"):\n        yield Content(ThemeColorButtons(), ThemeColorsView(), id=\"theme\")\n        yield Lazy(NamedColorsView())\n
    Parameters Parameter Default Description widget Widget required

    A widget that should be mounted after a refresh.

    "},{"location":"api/logger/","title":"Logger","text":"

    A logger class that logs to the Textual console.

    "},{"location":"api/logger/#textual.Logger","title":"textual.Logger class","text":"
    def __init__(\n    self,\n    log_callable,\n    group=LogGroup.INFO,\n    verbosity=LogVerbosity.NORMAL,\n):\n

    A Textual logger.

    "},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
    debug: Logger\n

    Logs debug messages.

    "},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
    error: Logger\n

    Logs errors.

    "},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
    event: Logger\n

    Logs events.

    "},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
    info: Logger\n

    Logs information.

    "},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
    logging: Logger\n

    Logs from stdlib logging module.

    "},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
    system: Logger\n

    Logs system information.

    "},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
    verbose: Logger\n

    A verbose logger.

    "},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
    warning: Logger\n

    Logs warnings.

    "},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
    worker: Logger\n

    Logs worker information.

    "},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity method","text":"
    def verbosity(self, verbose):\n

    Get a new logger with selective verbosity.

    Parameters Parameter Default Description verbose bool required

    True to use HIGH verbosity, otherwise NORMAL.

    Returns Type Description Logger

    New logger.

    "},{"location":"api/logging/","title":"Logging","text":"

    A Textual Logging handler.

    If there is an active Textual app, then log messages will go via the app (and logged via textual console).

    If there is no active app, then log messages will go to stderr or stdout, depending on configuration.

    "},{"location":"api/logging/#textual.logging.TextualHandler","title":"TextualHandler class","text":"
    def __init__(self, stderr=True, stdout=False):\n

    Bases: Handler

    A Logging handler for Textual apps.

    Parameters Parameter Default Description stderr bool True

    Log to stderr when there is no active app.

    stdout bool False

    Log to stdout when there is not active app.

    "},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit method","text":"
    def emit(self, record):\n

    Invoked by logging.

    "},{"location":"api/map_geometry/","title":"Map geometry","text":"

    A data structure returned by screen.find_widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry","title":"textual._compositor.MapGeometry class","text":"

    Bases: NamedTuple

    Defines the absolute location of a Widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.clip","title":"clip instance-attribute","text":"
    clip: Region\n

    A region to clip the widget by (if a Widget is within a container).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.container_size","title":"container_size instance-attribute","text":"
    container_size: Size\n

    The container size (area not occupied by scrollbars).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
    dock_gutter: Spacing\n

    Space from the container reserved by docked widgets.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.order","title":"order instance-attribute","text":"
    order: tuple[tuple[int, int, int], ...]\n

    Tuple of tuples defining the painting order of the widget.

    Each successive triple represents painting order information with regards to ancestors in the DOM hierarchy and the last triple provides painting order information for this specific widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.region","title":"region instance-attribute","text":"
    region: Region\n

    The (screen) region occupied by the widget.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
    virtual_region: Region\n

    The region relative to the container (but not necessarily visible).

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size: Size\n

    The virtual size (scrollable area) of a widget if it is a container.

    "},{"location":"api/map_geometry/#textual._compositor.MapGeometry.visible_region","title":"visible_region property","text":"
    visible_region: Region\n

    The Widget region after clipping.

    "},{"location":"api/message/","title":"Message","text":"

    The base class for all messages (including events).

    "},{"location":"api/message/#textual.message.Message","title":"Message class","text":"
    def __init__(self):\n

    Base class for a message.

    "},{"location":"api/message/#textual.message.Message.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute","text":"
    ALLOW_SELECTOR_MATCH: set[str] = set()\n

    Additional attributes that can be used with the on decorator.

    These attributes must be widgets.

    "},{"location":"api/message/#textual.message.Message.control","title":"control property","text":"
    control: DOMNode | None\n

    The widget associated with this message, or None by default.

    "},{"location":"api/message/#textual.message.Message.handler_name","title":"handler_name class-attribute","text":"
    handler_name: str\n

    Name of the default message handler.

    "},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
    is_forwarded: bool\n

    Has the message been forwarded?

    "},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default method","text":"
    def prevent_default(self, prevent=True):\n

    Suppress the default action(s). This will prevent handlers in any base classes from being called.

    Parameters Parameter Default Description prevent bool True

    True if the default action should be suppressed, or False if the default actions should be performed.

    "},{"location":"api/message/#textual.message.Message.stop","title":"stop method","text":"
    def stop(self, stop=True):\n

    Stop propagation of the message to parent.

    Parameters Parameter Default Description stop bool True

    The stop flag.

    "},{"location":"api/message_pump/","title":"Message pump","text":"

    A MessagePump is a base class for any object which processes messages, which includes Widget, Screen, and App.

    Tip

    Most of the method here are useful in general app development.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump","title":"MessagePump class","text":"
    def __init__(self, parent=None):\n

    Base class which supplies a message pump.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
    app: 'App[object]'\n

    Get the current app.

    Returns Type Description 'App[object]'

    The current app.

    Raises Type Description NoActiveAppError

    if no active app could be found for the current asyncio context

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.has_parent","title":"has_parent property","text":"
    has_parent: bool\n

    Does this object have a parent?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
    is_attached: bool\n

    Is the node attached to the app via the DOM?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
    is_parent_active: bool\n

    Is the parent active?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
    is_running: bool\n

    Is the message pump running (potentially processing messages)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
    log: Logger\n

    Get a logger for this object.

    Returns Type Description Logger

    A logger.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.message_queue_size","title":"message_queue_size property","text":"
    message_queue_size: int\n

    The current size of the message queue.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh method","text":"
    def call_after_refresh(self, callback, *args, **kwargs):\n

    Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable.

    Parameters Parameter Default Description callback Callback required

    A callable.

    Returns Type Description bool

    True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later method","text":"
    def call_later(self, callback, *args, **kwargs):\n

    Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable.

    Parameters Parameter Default Description callback Callback required

    Callable to call next.

    *args Any ()

    Positional arguments to pass to the callable.

    **kwargs Any {}

    Keyword arguments to pass to the callable.

    Returns Type Description bool

    True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next method","text":"
    def call_next(self, callback, *args, **kwargs):\n

    Schedule a callback to run immediately after processing the current message.

    Parameters Parameter Default Description callback Callback required

    Callable to run after current event.

    *args Any ()

    Positional arguments to pass to the callable.

    **kwargs Any {}

    Keyword arguments to pass to the callable.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle method","text":"
    def check_idle(self):\n

    Prompt the message pump to call idle if the queue is empty.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_message_enabled","title":"check_message_enabled method","text":"
    def check_message_enabled(self, message):\n

    Check if a given message is enabled (allowed to be sent).

    Parameters Parameter Default Description message Message required

    A message object.

    Returns Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages method","text":"
    def disable_messages(self, *messages):\n

    Disable message types from being processed.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.dispatch_key","title":"dispatch_key async","text":"
    def dispatch_key(self, event):\n

    Dispatch a key event to method.

    This method will call the method named 'key_' if it exists. Some keys have aliases. The first alias found will be invoked if it exists. If multiple handlers exist that match the key, an exception is raised. Parameters Parameter Default Description event events.Key required

    A key event.

    Returns Type Description bool

    True if key was handled, otherwise False.

    Raises Type Description DuplicateKeyHandlers

    When there's more than 1 handler that could handle this key.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages method","text":"
    def enable_messages(self, *messages):\n

    Enable processing of messages types.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
    def on_event(self, event):\n

    Called to process an event.

    Parameters Parameter Default Description event events.Event required

    An Event object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message method","text":"
    def post_message(self, message):\n

    Posts a message on to this widget's queue.

    Parameters Parameter Default Description message Message required

    A message (including Event).

    Returns Type Description bool

    True if the messages was processed, False if it wasn't.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent method","text":"
    def prevent(self, *message_types):\n

    A context manager to temporarily prevent the given message types from being posted.

    Example
    input = self.query_one(Input)\nwith self.prevent(Input.Changed):\n    input.value = \"foo\"\n
    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval","title":"set_interval method","text":"
    def set_interval(\n    self,\n    interval,\n    callback=None,\n    *,\n    name=None,\n    repeat=0,\n    pause=False\n):\n

    Call a function at periodic intervals.

    Parameters Parameter Default Description interval float required

    Time between calls.

    callback TimerCallback | None None

    Function to call.

    name str | None None

    Name of the timer object.

    repeat int 0

    Number of times to repeat the call or 0 for continuous.

    pause bool False

    Start the timer paused.

    Returns Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer method","text":"
    def set_timer(\n    self, delay, callback=None, *, name=None, pause=False\n):\n

    Make a function call after a delay.

    Parameters Parameter Default Description delay float required

    Time to wait before invoking callback.

    callback TimerCallback | None None

    Callback to call after time has expired.

    name str | None None

    Name of the timer (for debug).

    pause bool False

    Start timer paused.

    Returns Type Description Timer

    A timer object.

    "},{"location":"api/on/","title":"On","text":"

    Decorator to declare that the method is a message handler.

    The decorator accepts an optional CSS selector that will be matched against a widget exposed by a control property on the message.

    Example
    # Handle the press of buttons with ID \"#quit\".\n@on(Button.Pressed, \"#quit\")\ndef quit_button(self) -> None:\n    self.app.quit()\n

    Keyword arguments can be used to match additional selectors for attributes listed in ALLOW_SELECTOR_MATCH.

    Example
    # Handle the activation of the tab \"#home\" within the `TabbedContent` \"#tabs\".\n@on(TabbedContent.TabActivated, \"#tabs\", tab=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n
    Parameters Parameter Default Description message_type type[Message] required

    The message type (i.e. the class).

    selector str | None None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    **kwargs str {}

    Additional selectors for other attributes of the message.

    "},{"location":"api/pilot/","title":"Pilot","text":"

    The pilot object is used by App.run_test to programmatically operate an app.

    See the guide on how to test Textual apps.

    "},{"location":"api/pilot/#textual.pilot.OutOfBounds","title":"OutOfBounds class","text":"

    Bases: Exception

    Raised when the pilot mouse target is outside of the (visible) screen.

    "},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot class","text":"
    def __init__(self, app):\n

    Bases: Generic[ReturnType]

    Pilot object to drive an app.

    "},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
    app: App[ReturnType]\n
    "},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
    def click(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate clicking with the mouse at a specified position.

    The final position to be clicked is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Example

    The code below runs an app and clicks its only button right in the middle:

    async with SingleButtonApp().run_test() as pilot:\n    await pilot.click(Button, offset=(8, 1))\n

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the click offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to click on a specific widget. However, if the widget is currently hidden or obscured by another widget, the click may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset to click. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Click with the shift key held down.

    meta bool False

    Click with the meta key held down.

    control bool False

    Click with the control key held down.

    Raises Type Description OutOfBounds

    If the position to be clicked is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the click landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
    def exit(self, result):\n

    Exit the app with the given result.

    Parameters Parameter Default Description result ReturnType required

    The app result returned by run or run_async.

    "},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
    def hover(self, selector=None, offset=(0, 0)):\n

    Simulate hovering with the mouse cursor at a specified position.

    The final position to be hovered is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None | None None

    A selector to specify a widget that should be used as the reference for the hover offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to hover a specific widget. However, if the widget is currently hidden or obscured by another widget, the hover may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset to hover. The offset is relative to the selector provided or to the screen, if no selector is provided.

    Raises Type Description OutOfBounds

    If the position to be hovered is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the hover landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down","title":"mouse_down async","text":"
    def mouse_down(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate a MouseDown event at a specified position.

    The final position for the event is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the event offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to target a specific widget. However, if the widget is currently hidden or obscured by another widget, the event may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Simulate the event with the shift key held down.

    meta bool False

    Simulate the event with the meta key held down.

    control bool False

    Simulate the event with the control key held down.

    Raises Type Description OutOfBounds

    If the position for the event is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the event landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up","title":"mouse_up async","text":"
    def mouse_up(\n    self,\n    selector=None,\n    offset=(0, 0),\n    shift=False,\n    meta=False,\n    control=False,\n):\n

    Simulate a MouseUp event at a specified position.

    The final position for the event is computed based on the selector provided and the offset specified and it must be within the visible area of the screen.

    Parameters Parameter Default Description selector type[Widget] | str | None None

    A selector to specify a widget that should be used as the reference for the event offset. If this is not specified, the offset is interpreted relative to the screen. You can use this parameter to try to target a specific widget. However, if the widget is currently hidden or obscured by another widget, the event may not land on the widget you specified.

    offset tuple[int, int] (0, 0)

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    shift bool False

    Simulate the event with the shift key held down.

    meta bool False

    Simulate the event with the meta key held down.

    control bool False

    Simulate the event with the control key held down.

    Raises Type Description OutOfBounds

    If the position for the event is outside of the (visible) screen.

    Returns Type Description bool

    True if no selector was specified or if the event landed on the selected widget, False otherwise.

    "},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
    def pause(self, delay=None):\n

    Insert a pause.

    Parameters Parameter Default Description delay float | None None

    Seconds to pause, or None to wait for cpu idle.

    "},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
    def press(self, *keys):\n

    Simulate key-presses.

    Parameters Parameter Default Description *keys str ()

    Keys to press.

    "},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
    def wait_for_animation(self):\n

    Wait for any current animation to complete.

    "},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_scheduled_animations","title":"wait_for_scheduled_animations async","text":"
    def wait_for_scheduled_animations(self):\n

    Wait for any current and scheduled animations to complete.

    "},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout class","text":"

    Bases: Exception

    Exception raised if messages aren't being processed quickly enough.

    If this occurs, the most likely explanation is some kind of deadlock in the app code.

    "},{"location":"api/query/","title":"Query","text":"

    A DOMQuery is a set of DOM nodes returned by query.

    The set of nodes may be further refined with filter and exclude. Additional methods apply actions to all nodes in the query.

    Info

    If this sounds like JQuery, a (once) popular JS library, it is no coincidence.

    "},{"location":"api/query/#textual.css.query.ExpectType","title":"ExpectType module-attribute","text":"
    ExpectType = TypeVar('ExpectType')\n

    Type variable used to further restrict queries.

    "},{"location":"api/query/#textual.css.query.QueryType","title":"QueryType module-attribute","text":"
    QueryType = TypeVar('QueryType', bound='Widget')\n

    Type variable used to type generic queries.

    "},{"location":"api/query/#textual.css.query.DOMQuery","title":"DOMQuery class","text":"
    def __init__(\n    self, node, *, filter=None, exclude=None, parent=None\n):\n

    Bases: Generic[QueryType]

    Warning

    You won't need to construct this manually, as DOMQuery objects are returned by query.

    Parameters Parameter Default Description node DOMNode required

    A DOM node.

    filter str | None None

    Query to filter children in the node.

    exclude str | None None

    Query to exclude children in the node.

    parent DOMQuery | None None

    The parent query, if this is the result of filtering another query.

    Raises Type Description InvalidQueryFormat

    If the format of the query is invalid.

    "},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
    node: DOMNode\n

    The node being queried.

    "},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
    nodes: list[QueryType]\n

    Lazily evaluate nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class method","text":"
    def add_class(self, *class_names):\n

    Add the given class name(s) to nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.blur","title":"blur method","text":"
    def blur(self):\n

    Blur the first matching node that is focused.

    Returns Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.exclude","title":"exclude method","text":"
    def exclude(self, selector):\n

    Exclude nodes that match a given selector.

    Parameters Parameter Default Description selector str required

    A CSS selector.

    Returns Type Description DOMQuery[QueryType]

    New DOM query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter method","text":"
    def filter(self, selector):\n

    Filter this set by the given CSS selector.

    Parameters Parameter Default Description selector str required

    A CSS selector.

    Returns Type Description DOMQuery[QueryType]

    New DOM Query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first method","text":"
    def first(self, expect_type=None):\n

    Get the first matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If there are no matching nodes in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.focus","title":"focus method","text":"
    def focus(self):\n

    Focus the first matching node that permits focus.

    Returns Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.last","title":"last method","text":"
    def last(self, expect_type=None):\n

    Get the last matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If there are no matching nodes in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one method","text":"
    def only_one(self, expect_type=None):\n

    Get the only matching node.

    Parameters Parameter Default Description expect_type type[ExpectType] | None None

    Require matched node is of this type, or None for any type.

    Raises Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    TooManyMatches

    If there is more than one matching node in the query.

    Returns Type Description QueryType | ExpectType

    The matching Widget.

    "},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh method","text":"
    def refresh(self, *, repaint=True, layout=False):\n

    Refresh matched nodes.

    Parameters Parameter Default Description repaint bool True

    Repaint node(s).

    layout bool False

    Layout node(s).

    Returns Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove method","text":"
    def remove(self):\n

    Remove matched nodes from the DOM.

    Returns Type Description AwaitRemove

    An awaitable object that waits for the widgets to be removed.

    "},{"location":"api/query/#textual.css.query.DOMQuery.remove_class","title":"remove_class method","text":"
    def remove_class(self, *class_names):\n

    Remove the given class names from the nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results method","text":"
    def results(self, filter_type=None):\n

    Get query results, optionally filtered by a given type.

    Parameters Parameter Default Description filter_type type[ExpectType] | None None

    A Widget class to filter results, or None for no filter.

    Yields:

    Type Description QueryType | ExpectType

    Iterator[Widget | ExpectType]: An iterator of Widget instances.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class method","text":"
    def set_class(self, add, *class_names):\n

    Set the given class name(s) according to a condition.

    Parameters Parameter Default Description add bool required

    Add the classes if True, otherwise remove them.

    Returns Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes method","text":"
    def set_classes(self, classes):\n

    Set the classes on nodes to exactly the given set.

    Parameters Parameter Default Description classes str | Iterable[str] required

    A string of space separated classes, or an iterable of class names.

    Returns Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles method","text":"
    def set_styles(self, css=None, **update_styles):\n

    Set styles on matched nodes.

    Parameters Parameter Default Description css str | None None

    CSS declarations to parser, or None.

    "},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class method","text":"
    def toggle_class(self, *class_names):\n

    Toggle the given class names from matched nodes.

    "},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat class","text":"

    Bases: QueryError

    Query did not parse correctly.

    "},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches class","text":"

    Bases: QueryError

    No nodes matched the query.

    "},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError class","text":"

    Bases: Exception

    Base class for a query related error.

    "},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches class","text":"

    Bases: QueryError

    Too many nodes matched the query.

    "},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType class","text":"

    Bases: QueryError

    Query result was not of the correct type.

    "},{"location":"api/reactive/","title":"Reactive","text":"

    The Reactive class implements reactivity.

    "},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive class","text":"
    def __init__(\n    self,\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=False,\n    always_update=False,\n    compute=True\n):\n

    Bases: Generic[ReactiveType]

    Reactive descriptor.

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    layout bool False

    Perform a layout on change.

    repaint bool True

    Perform a repaint on change.

    init bool False

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    compute bool True

    Run compute methods when attribute is changed.

    "},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError class","text":"

    Bases: Exception

    Raised when an attribute has public and private compute methods.

    "},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive class","text":"
    def __init__(\n    self,\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=True,\n    always_update=False\n):\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute.

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    layout bool False

    Perform a layout on change.

    repaint bool True

    Perform a repaint on change.

    init bool True

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    "},{"location":"api/reactive/#textual.reactive.var","title":"var class","text":"
    def __init__(self, default, init=True, always_update=False):\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute (with no auto-refresh).

    Parameters Parameter Default Description default ReactiveType | Callable[[], ReactiveType] required

    A default value or callable that returns a default.

    init bool True

    Call watchers on initialize (post mount).

    always_update bool False

    Call watchers even when the new value equals the old value.

    "},{"location":"api/renderables/","title":"Renderables","text":"

    A collection of Rich renderables which may be returned from a widget's render() method.

    "},{"location":"api/renderables/#textual.renderables.bar.Bar","title":"Bar class","text":"
    def __init__(\n    self,\n    highlight_range=(0, 0),\n    highlight_style=\"magenta\",\n    background_style=\"grey37\",\n    clickable_ranges=None,\n    width=None,\n):\n

    Thin horizontal bar with a portion highlighted.

    Parameters Parameter Default Description highlight_range tuple[float, float] (0, 0)

    The range to highlight.

    highlight_style StyleType 'magenta'

    The style of the highlighted range of the bar.

    background_style StyleType 'grey37'

    The style of the non-highlighted range(s) of the bar.

    width int | None None

    The width of the bar, or None to fill available width.

    "},{"location":"api/renderables/#textual.renderables.blank.Blank","title":"Blank class","text":"
    def __init__(self, color='transparent'):\n

    Draw solid background color.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits","title":"Digits class","text":"
    def __init__(self, text, style=''):\n

    Renders a 3X3 unicode 'font' for numerical values.

    Parameters Parameter Default Description text str required

    Text to display.

    style StyleType ''

    Style to apply to the digits.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width","title":"get_width classmethod","text":"
    def get_width(cls, text):\n

    Calculate the width without rendering.

    Parameters Parameter Default Description text str required

    Text which may be displayed in the Digits widget.

    Returns Type Description int

    width of the text (in cells).

    "},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient","title":"LinearGradient class","text":"
    def __init__(self, angle, stops):\n

    Render a linear gradient with a rotation.

    Parameters Parameter Default Description angle float required

    Angle of rotation in degrees.

    stops Sequence[tuple[float, Color | str]] required

    List of stop consisting of pairs of offset (between 0 and 1) and color.

    "},{"location":"api/renderables/#textual.renderables.gradient.VerticalGradient","title":"VerticalGradient class","text":"
    def __init__(self, color1, color2):\n

    Draw a vertical gradient.

    "},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline","title":"Sparkline class","text":"
    def __init__(\n    self,\n    data,\n    *,\n    width,\n    min_color=Color.from_rgb(0, 255, 0),\n    max_color=Color.from_rgb(255, 0, 0),\n    summary_function=max\n):\n

    Bases: Generic[T]

    A sparkline representing a series of data.

    Parameters Parameter Default Description data Sequence[T] required

    The sequence of data to render.

    width int | None required

    The width of the sparkline/the number of buckets to partition the data into.

    min_color Color Color.from_rgb(0, 255, 0)

    The color of values equal to the min value in data.

    max_color Color Color.from_rgb(255, 0, 0)

    The color of values equal to the max value in data.

    summary_function SummaryFunction[T] max

    Function that will be applied to each bucket.

    "},{"location":"api/screen/","title":"Screen","text":"

    The Screen class is a special widget which represents the content in the terminal. See Screens for details.

    "},{"location":"api/screen/#textual.screen.ScreenResultCallbackType","title":"ScreenResultCallbackType module-attribute","text":"
    ScreenResultCallbackType = Union[\n    Callable[[ScreenResultType], None],\n    Callable[[ScreenResultType], Awaitable[None]],\n]\n

    Type of a screen result callback function.

    "},{"location":"api/screen/#textual.screen.ScreenResultType","title":"ScreenResultType module-attribute","text":"
    ScreenResultType = TypeVar('ScreenResultType')\n

    The result type of a screen.

    "},{"location":"api/screen/#textual.screen.ModalScreen","title":"ModalScreen class","text":"
    def __init__(self, name=None, id=None, classes=None):\n

    Bases: Screen[ScreenResultType]

    A screen with bindings that take precedence over the App's key bindings.

    The default styling of a modal screen will dim the screen underneath.

    "},{"location":"api/screen/#textual.screen.ResultCallback","title":"ResultCallback class","text":"
    def __init__(self, requester, callback, future=None):\n

    Bases: Generic[ScreenResultType]

    Holds the details of a callback.

    Parameters Parameter Default Description requester MessagePump required

    The object making a request for the callback.

    callback ScreenResultCallbackType[ScreenResultType] | None required

    The callback function.

    future asyncio.Future[ScreenResultType] | None None

    A Future to hold the result.

    "},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
    callback: ScreenResultCallbackType | None = callback\n

    The callback function.

    "},{"location":"api/screen/#textual.screen.ResultCallback.future","title":"future instance-attribute","text":"
    future = future\n

    A future for the result

    "},{"location":"api/screen/#textual.screen.ResultCallback.requester","title":"requester instance-attribute","text":"
    requester = requester\n

    The object in the DOM that requested the callback.

    "},{"location":"api/screen/#textual.screen.Screen","title":"Screen class","text":"
    def __init__(self, name=None, id=None, classes=None):\n

    Bases: Generic[ScreenResultType], Widget

    The base class for screens.

    Parameters Parameter Default Description name str | None None

    The name of the screen.

    id str | None None

    The ID of the screen in the DOM.

    classes str | None None

    The CSS classes for the screen.

    "},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS: str | None = None\n

    A selector to determine what to focus automatically when the screen is activated.

    The widget focused is the first that matches the given CSS selector. Set to None to inherit the value from the screen's app. Set to \"\" to disable auto focus.

    "},{"location":"api/screen/#textual.screen.Screen.COMMANDS","title":"COMMANDS class-attribute","text":"
    COMMANDS: set[\n    type[Provider] | Callable[[], type[Provider]]\n] = set()\n

    Command providers used by the command palette, associated with the screen.

    Should be a set of command.Provider classes.

    "},{"location":"api/screen/#textual.screen.Screen.CSS","title":"CSS class-attribute","text":"
    CSS: str = ''\n

    Inline CSS, useful for quick scripts. Rules here take priority over CSS_PATH.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.CSS_PATH","title":"CSS_PATH class-attribute","text":"
    CSS_PATH: CSSPathType | None = None\n

    File paths to load CSS from.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
    SUB_TITLE: str | None = None\n

    A class variable to set the default sub-title for the screen.

    This overrides the app sub-title. To update the sub-title while the screen is running, you can set the sub_title attribute.

    "},{"location":"api/screen/#textual.screen.Screen.TITLE","title":"TITLE class-attribute","text":"
    TITLE: str | None = None\n

    A class variable to set the default title for the screen.

    This overrides the app title. To update the title while the screen is running, you can set the title attribute.

    "},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
    focus_chain: list[Widget]\n

    A list of widgets that may receive focus, in focus order.

    "},{"location":"api/screen/#textual.screen.Screen.focused","title":"focused class-attribute instance-attribute","text":"
    focused: Reactive[Widget | None] = Reactive(None)\n

    The focused widget or None for no focus.

    "},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
    is_current: bool\n

    Is the screen current (i.e. visible to user)?

    "},{"location":"api/screen/#textual.screen.Screen.is_modal","title":"is_modal property","text":"
    is_modal: bool\n

    Is the screen modal?

    "},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
    layers: tuple[str, ...]\n

    Layers from parent.

    Returns Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates class-attribute instance-attribute","text":"
    stack_updates: Reactive[int] = Reactive(0, repaint=False)\n

    An integer that updates when the screen is resumed.

    "},{"location":"api/screen/#textual.screen.Screen.sub_title","title":"sub_title class-attribute instance-attribute","text":"
    sub_title: Reactive[str | None] = self.SUB_TITLE\n

    Screen sub-title to override the app sub-title.

    "},{"location":"api/screen/#textual.screen.Screen.title","title":"title class-attribute instance-attribute","text":"
    title: Reactive[str | None] = self.TITLE\n

    Screen title to override the app title.

    "},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss method","text":"
    def action_dismiss(self, result=_NoResult):\n

    A wrapper around dismiss that can be called as an action.

    Parameters Parameter Default Description result ScreenResultType | Type[_NoResult] _NoResult

    The optional result to be passed to the result callback.

    "},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view method","text":"
    def can_view(self, widget):\n

    Check if a given widget is in the current view (scrollable area).

    Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

    Parameters Parameter Default Description widget Widget required

    A widget that is a descendant of self.

    Returns Type Description bool

    True if the entire widget is in view, False if it is partially visible or not in view.

    "},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss method","text":"
    def dismiss(self, result=_NoResult):\n

    Dismiss the screen, optionally with a result.

    If result is provided and a callback was set when the screen was pushed, then the callback will be invoked with result.

    Parameters Parameter Default Description result ScreenResultType | Type[_NoResult] _NoResult

    The optional result to be passed to the result callback.

    Raises Type Description ScreenStackError

    If trying to dismiss a screen that is not at the top of the stack.

    "},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget method","text":"
    def find_widget(self, widget):\n

    Get the screen region of a Widget.

    Parameters Parameter Default Description widget Widget required

    A Widget within the composition.

    Returns Type Description MapGeometry

    Region relative to screen.

    Raises Type Description NoWidget

    If the widget could not be found in this screen.

    "},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next method","text":"
    def focus_next(self, selector='*'):\n

    Focus the next widget, optionally filtered by a CSS selector.

    If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

    Parameters Parameter Default Description selector str | type[QueryType] '*'

    CSS selector to filter what nodes can be focused.

    Returns Type Description Widget | None

    Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

    "},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous method","text":"
    def focus_previous(self, selector='*'):\n

    Focus the previous widget, optionally filtered by a CSS selector.

    If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

    Parameters Parameter Default Description selector str | type[QueryType] '*'

    CSS selector to filter what nodes can be focused.

    Returns Type Description Widget | None

    Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

    "},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset method","text":"
    def get_offset(self, widget):\n

    Get the absolute offset of a given Widget.

    Parameters Parameter Default Description widget Widget required

    A widget

    Returns Type Description Offset

    The widget's offset relative to the top left of the terminal.

    "},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at method","text":"
    def get_style_at(self, x, y):\n

    Get the style under a given coordinate.

    Parameters Parameter Default Description x int required

    X Coordinate.

    y int required

    Y Coordinate.

    Returns Type Description Style

    Rich Style object.

    "},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at method","text":"
    def get_widget_at(self, x, y):\n

    Get the widget at a given coordinate.

    Parameters Parameter Default Description x int required

    X Coordinate.

    y int required

    Y Coordinate.

    Returns Type Description tuple[Widget, Region]

    Widget and screen region.

    "},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at method","text":"
    def get_widgets_at(self, x, y):\n

    Get all widgets under a given coordinate.

    Parameters Parameter Default Description x int required

    X coordinate.

    y int required

    Y coordinate.

    Returns Type Description Iterable[tuple[Widget, Region]]

    Sequence of (WIDGET, REGION) tuples.

    "},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus method","text":"
    def set_focus(self, widget, scroll_visible=True):\n

    Focus (or un-focus) a widget. A focused widget will receive key events first.

    Parameters Parameter Default Description widget Widget | None required

    Widget to focus, or None to un-focus.

    scroll_visible bool True

    Scroll widget in to view.

    "},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title method","text":"
    def validate_sub_title(self, sub_title):\n

    Ensure the sub-title is a string or None.

    "},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title method","text":"
    def validate_title(self, title):\n

    Ensure the title is a string or None.

    "},{"location":"api/scroll_view/","title":"Scroll view","text":"

    ScrollView is a base class for line api widgets.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView class","text":"

    Bases: ScrollableContainer

    A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable: bool\n

    Always scrollable.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines method","text":"
    def refresh_lines(self, y_start, line_count=1):\n

    Refresh one or more lines.

    Parameters Parameter Default Description y_start int required

    First line to refresh.

    line_count int 1

    Total number of lines to refresh.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to method","text":"
    def scroll_to(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters Parameter Default Description x float | None None

    X coordinate (column) to scroll to, or None for no change.

    y float | None None

    Y coordinate (row) to scroll to, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/scrollbar/","title":"Scrollbar","text":"

    Implements the scrollbar-related widgets for internal use.

    You will not need to use the widgets defined in this module.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar class","text":"
    def __init__(self, vertical=True, name=None, *, thickness=1):\n

    Bases: Widget

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
    renderer: Type[ScrollBarRender] = ScrollBarRender\n

    The class used for rendering scrollbars. This can be overridden and set to a ScrollBarRender-derived class in order to delegate all scrollbar rendering to that class. E.g.:

    class MyScrollBarRender(ScrollBarRender): ...\n\napp = MyApp()\nScrollBar.renderer = MyScrollBarRender\napp.run()\n

    Because this variable is accessed through specific instances (rather than through the class ScrollBar itself) it is also possible to set this on specific scrollbar instance to change only that instance:

    my_widget.horizontal_scrollbar.renderer = MyScrollBarRender\n
    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_grab","title":"action_grab method","text":"
    def action_grab(self):\n

    Begin capturing the mouse cursor.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down method","text":"
    def action_scroll_down(self):\n

    Scroll vertical scrollbars down, horizontal scrollbars right.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up method","text":"
    def action_scroll_up(self):\n

    Scroll vertical scrollbars up, horizontal scrollbars left.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner class","text":"
    def __init__(self, name=None):\n

    Bases: Widget

    Widget which fills the gap between horizontal and vertical scrollbars, should they both be present.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown class","text":"

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft class","text":"

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage class","text":"

    Bases: Message

    Base class for all scrollbar messages.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight class","text":"

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo class","text":"
    def __init__(self, x=None, y=None, animate=True):\n

    Bases: ScrollMessage

    Message sent when click and dragging handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp class","text":"

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/signal/","title":"Signal","text":"

    Signals are a simple pub-sub mechanism.

    DOMNodes can subscribe to a signal, which will invoke a callback when the signal is published.

    This is experimental for now, for internal use. It may be part of the public API in a future release.

    "},{"location":"api/signal/#textual.signal.Signal","title":"Signal class","text":"
    def __init__(self, owner, name):\n

    A signal that a widget may subscribe to, in order to invoke callbacks when an associated event occurs.

    Parameters Parameter Default Description owner DOMNode required

    The owner of this signal.

    name str required

    An identifier for debugging purposes.

    "},{"location":"api/signal/#textual.signal.Signal.publish","title":"publish method","text":"
    def publish(self):\n

    Publish the signal (invoke subscribed callbacks).

    "},{"location":"api/signal/#textual.signal.Signal.subscribe","title":"subscribe method","text":"
    def subscribe(self, node, callback):\n

    Subscribe a node to this signal.

    When the signal is published, the callback will be invoked.

    Parameters Parameter Default Description node DOMNode required

    Node to subscribe.

    callback IgnoreReturnCallbackType required

    A callback function which takes no arguments, and returns anything (return type ignored).

    Raises Type Description SignalError

    Raised when subscribing a non-mounted widget.

    "},{"location":"api/signal/#textual.signal.Signal.unsubscribe","title":"unsubscribe method","text":"
    def unsubscribe(self, node):\n

    Unsubscribe a node from this signal.

    Parameters Parameter Default Description node DOMNode required

    Node to unsubscribe,

    "},{"location":"api/signal/#textual.signal.SignalError","title":"SignalError class","text":"

    Bases: Exception

    Raised for Signal errors.

    "},{"location":"api/strip/","title":"Strip","text":"

    A Strip contains the result of rendering a widget. See line API for how to use Strips.

    "},{"location":"api/strip/#textual.strip.Strip","title":"Strip class","text":"
    def __init__(self, segments, cell_length=None):\n

    Represents a 'strip' (horizontal line) of a Textual Widget.

    A Strip is like an immutable list of Segments. The immutability allows for effective caching.

    Parameters Parameter Default Description segments Iterable[Segment] required

    An iterable of segments.

    cell_length int | None None

    The cell length if known, or None to calculate on demand.

    "},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
    cell_length: int\n

    Get the number of cells required to render this object.

    "},{"location":"api/strip/#textual.strip.Strip.link_ids","title":"link_ids property","text":"
    link_ids: set[str]\n

    A set of the link ids in this Strip.

    "},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
    text: str\n

    Segment text.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length method","text":"
    def adjust_cell_length(self, cell_length, style=None):\n

    Adjust the cell length, possibly truncating or extending.

    Parameters Parameter Default Description cell_length int required

    New desired cell length.

    style Style | None None

    Style when extending, or None.

    Returns Type Description Strip

    A new strip with the supplied cell length.

    "},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter method","text":"
    def apply_filter(self, filter, background):\n

    Apply a filter to all segments in the strip.

    Parameters Parameter Default Description filter LineFilter required

    A line filter object.

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style method","text":"
    def apply_style(self, style):\n

    Apply a style to the Strip.

    Parameters Parameter Default Description style Style required

    A Rich style.

    Returns Type Description Strip

    A new strip.

    "},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
    def blank(cls, cell_length, style=None):\n

    Create a blank strip.

    Parameters Parameter Default Description cell_length int required

    Desired cell length.

    style StyleType | None None

    Style of blank.

    Returns Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop method","text":"
    def crop(self, start, end=None):\n

    Crop a strip between two cell positions.

    Parameters Parameter Default Description start int required

    The start cell position (inclusive).

    end int | None None

    The end cell position (exclusive).

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend method","text":"
    def crop_extend(self, start, end, style):\n

    Crop between two points, extending the length if required.

    Parameters Parameter Default Description start int required

    Start offset of crop.

    end int required

    End offset of crop.

    style Style | None required

    Style of additional padding.

    Returns Type Description Strip

    New cropped Strip.

    "},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide method","text":"
    def divide(self, cuts):\n

    Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

    Parameters Parameter Default Description cuts Iterable[int] required

    An iterable of cell positions as ints.

    Returns Type Description Sequence[Strip]

    A new list of strips.

    "},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length method","text":"
    def extend_cell_length(self, cell_length, style=None):\n

    Extend the cell length if it is less than the given value.

    Parameters Parameter Default Description cell_length int required

    Required minimum cell length.

    style Style | None None

    Style for padding if the cell length is extended.

    Returns Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
    def from_lines(cls, lines, cell_length=None):\n

    Convert lines (lists of segments) to a list of Strips.

    Parameters Parameter Default Description lines list[list[Segment]] required

    List of lines, where a line is a list of segments.

    cell_length int | None None

    Cell length of lines (must be same) or None if not known.

    Returns Type Description list[Strip]

    List of strips.

    "},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position method","text":"
    def index_to_cell_position(self, index):\n

    Given a character index, return the cell position of that character. This is the sum of the cell lengths of all the characters before the character at index.

    Parameters Parameter Default Description index int required

    The index to convert.

    Returns Type Description int

    The cell position of the character at index.

    "},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
    def join(cls, strips):\n

    Join a number of strips in to one.

    Parameters Parameter Default Description strips Iterable[Strip | None] required

    An iterable of Strips.

    Returns Type Description Strip

    A new combined strip.

    "},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify method","text":"
    def simplify(self):\n

    Simplify the segments (join segments with same style)

    Returns Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.style_links","title":"style_links method","text":"
    def style_links(self, link_id, link_style):\n

    Apply a style to Segments with the given link_id.

    Parameters Parameter Default Description link_id str required

    A link id.

    link_style Style required

    Style to apply.

    Returns Type Description Strip

    New strip (or same Strip if no changes).

    "},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable class","text":"
    def __init__(self, strips, width=None):\n

    A renderable which renders a list of strips in to lines.

    "},{"location":"api/strip/#textual.strip.get_line_length","title":"get_line_length function","text":"
    def get_line_length(segments):\n

    Get the line length (total length of all segments).

    Parameters Parameter Default Description segments Iterable[Segment] required

    Iterable of segments.

    Returns Type Description int

    Length of line in cells.

    "},{"location":"api/suggester/","title":"Suggester","text":"

    The Suggester class is used by the Input widget.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList class","text":"
    def __init__(self, suggestions, *, case_sensitive=True):\n

    Bases: Suggester

    Give completion suggestions based on a fixed list of options.

    Example
    countries = [\"England\", \"Scotland\", \"Portugal\", \"Spain\", \"France\"]\n\nclass MyApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield Input(suggester=SuggestFromList(countries, case_sensitive=False))\n

    If the user types P inside the input widget, a completion suggestion for \"Portugal\" appears.

    Parameters Parameter Default Description suggestions Iterable[str] required

    Valid suggestions sorted by decreasing priority.

    case_sensitive bool True

    Whether suggestions are computed in a case sensitive manner or not. The values provided in the argument suggestions represent the canonical representation of the completions and they will be suggested with that same casing.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
    def get_suggestion(self, value):\n

    Gets a completion from the given possibilities.

    Parameters Parameter Default Description value str required

    The current value.

    Returns Type Description str | None

    A valid completion suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester class","text":"
    def __init__(self, *, use_cache=True, case_sensitive=False):\n

    Bases: ABC

    Defines how widgets generate completion suggestions.

    To define a custom suggester, subclass Suggester and implement the async method get_suggestion. See SuggestFromList for an example.

    Parameters Parameter Default Description use_cache bool True

    Whether to cache suggestion results.

    case_sensitive bool False

    Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

    "},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
    cache: LRUCache[str, str | None] | None = (\n    LRUCache(1024) if use_cache else None\n)\n

    Suggestion cache, if used.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion async abstractmethod","text":"
    def get_suggestion(self, value):\n

    Try to get a completion suggestion for the given input value.

    Custom suggesters should implement this method.

    Note

    The value argument will be casefolded if self.case_sensitive is False.

    Note

    If your implementation is not deterministic, you may need to disable caching.

    Parameters Parameter Default Description value str required

    The current value of the requester widget.

    Returns Type Description str | None

    A valid suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady class","text":"

    Bases: Message

    Sent when a completion suggestion is ready.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
    suggestion: str\n

    The string suggestion.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
    value: str\n

    The value to which the suggestion is for.

    "},{"location":"api/system_commands_source/","title":"System commands source","text":"

    A command palette command provider for Textual system commands.

    This is a simple command provider that makes the most obvious application actions available via the command palette.

    "},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands","title":"SystemCommands class","text":"

    Bases: Provider

    A source of command palette commands that run app-wide tasks.

    Used by default in App.COMMANDS.

    "},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands.search","title":"search async","text":"
    def search(self, query):\n

    Handle a request to search for system commands that match the query.

    Parameters Parameter Default Description query str required

    The user input to be matched.

    Yields:

    Type Description Hits

    Command hits for use in the command palette.

    "},{"location":"api/timer/","title":"Timer","text":"

    Timer objects are created by set_interval or set_timer.

    "},{"location":"api/timer/#textual.timer.TimerCallback","title":"TimerCallback module-attribute","text":"
    TimerCallback = Union[\n    Callable[[], Awaitable[Any]], Callable[[], Any]\n]\n

    Type of valid callbacks to be used with timers.

    "},{"location":"api/timer/#textual.timer.Timer","title":"Timer class","text":"
    def __init__(\n    self,\n    event_target,\n    interval,\n    *,\n    name=None,\n    callback=None,\n    repeat=None,\n    skip=True,\n    pause=False\n):\n

    A class to send timer-based events.

    Parameters Parameter Default Description event_target MessageTarget required

    The object which will receive the timer events.

    interval float required

    The time between timer events, in seconds.

    name str | None None

    A name to assign the event (for debugging).

    callback TimerCallback | None None

    A optional callback to invoke when the event is handled.

    repeat int | None None

    The number of times to repeat the timer, or None to repeat forever.

    skip bool True

    Enable skipping of scheduled events that couldn't be sent in time.

    pause bool False

    Start the timer paused.

    "},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause method","text":"
    def pause(self):\n

    Pause the timer.

    A paused timer will not send events until it is resumed.

    "},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset method","text":"
    def reset(self):\n

    Reset the timer, so it starts from the beginning.

    "},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume method","text":"
    def resume(self):\n

    Resume a paused timer.

    "},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop method","text":"
    def stop(self):\n

    Stop the timer.

    "},{"location":"api/types/","title":"Types","text":"

    Export some objects that are used by Textual and that help document other features.

    "},{"location":"api/types/#textual.types.ActionParseResult","title":"ActionParseResult module-attribute","text":"
    ActionParseResult: TypeAlias = \"tuple[str, tuple[Any, ...]]\"\n

    An action is its name and the arbitrary tuple of its arguments.

    "},{"location":"api/types/#textual.types.CSSPathType","title":"CSSPathType module-attribute","text":"
    CSSPathType: TypeAlias = Union[\n    str, PurePath, List[Union[str, PurePath]]\n]\n

    Valid ways of specifying paths to CSS files.

    "},{"location":"api/types/#textual.types.CallbackType","title":"CallbackType module-attribute","text":"
    CallbackType = Union[\n    Callable[[], Awaitable[None]], Callable[[], None]\n]\n

    Type used for arbitrary callables used in callbacks.

    "},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
    EasingFunction = Callable[[float], float]\n

    Signature for a function that parametrises animation speed.

    An easing function must map the interval [0, 1] into the interval [0, 1].

    "},{"location":"api/types/#textual.types.IgnoreReturnCallbackType","title":"IgnoreReturnCallbackType module-attribute","text":"
    IgnoreReturnCallbackType = Union[\n    Callable[[], Awaitable[Any]], Callable[[], Any]\n]\n

    A callback which ignores the return type.

    "},{"location":"api/types/#textual.types.InputValidationOn","title":"InputValidationOn module-attribute","text":"
    InputValidationOn = Literal['blur', 'changed', 'submitted']\n

    Possible messages that trigger input validation.

    "},{"location":"api/types/#textual.types.NewOptionListContent","title":"NewOptionListContent module-attribute","text":"
    NewOptionListContent: TypeAlias = (\n    \"OptionListContent | None | RenderableType\"\n)\n

    The type of a new item of option list content to be added to an option list.

    This type represents all of the types that will be accepted when adding new content to the option list. This is a superset of OptionListContent.

    "},{"location":"api/types/#textual.types.OptionListContent","title":"OptionListContent module-attribute","text":"
    OptionListContent: TypeAlias = 'Option | Separator'\n

    The type of an item of content in the option list.

    This type represents all of the types that will be found in the list of content of the option list after it has been processed for addition.

    "},{"location":"api/types/#textual.types.PlaceholderVariant","title":"PlaceholderVariant module-attribute","text":"
    PlaceholderVariant = Literal['default', 'size', 'text']\n

    The different variants of placeholder.

    "},{"location":"api/types/#textual.types.SelectType","title":"SelectType module-attribute","text":"
    SelectType = TypeVar('SelectType')\n

    The type used for data in the Select.

    "},{"location":"api/types/#textual.types.WatchCallbackType","title":"WatchCallbackType module-attribute","text":"
    WatchCallbackType = Union[\n    Callable[[], Awaitable[None]],\n    Callable[[Any], Awaitable[None]],\n    Callable[[Any, Any], Awaitable[None]],\n    Callable[[], None],\n    Callable[[Any], None],\n    Callable[[Any, Any], None],\n]\n

    Type used for callbacks passed to the watch method of widgets.

    "},{"location":"api/types/#textual.types.Animatable","title":"Animatable class","text":"

    Bases: Protocol

    Protocol for objects that can have their intrinsic values animated.

    For example, the transition between two colors can be animated because the class Color satisfies this protocol.

    "},{"location":"api/types/#textual.types.CSSPathError","title":"CSSPathError class","text":"

    Bases: Exception

    Raised when supplied CSS path(s) are invalid.

    "},{"location":"api/types/#textual.types.DirEntry","title":"DirEntry class","text":"

    Attaches directory information to a DirectoryTree node.

    "},{"location":"api/types/#textual.widgets._directory_tree.DirEntry.loaded","title":"loaded class-attribute instance-attribute","text":"
    loaded: bool = False\n

    Has this been loaded?

    "},{"location":"api/types/#textual.widgets._directory_tree.DirEntry.path","title":"path instance-attribute","text":"
    path: Path\n

    The path of the directory entry.

    "},{"location":"api/types/#textual.types.DuplicateID","title":"DuplicateID class","text":"

    Bases: Exception

    Raised if a duplicate ID is used when adding options to an option list.

    "},{"location":"api/types/#textual.types.MessageTarget","title":"MessageTarget class","text":"

    Bases: Protocol

    Protocol that must be followed by objects that can receive messages.

    "},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError class","text":"

    Bases: RuntimeError

    Runtime error raised if we try to retrieve the active app when there is none.

    "},{"location":"api/types/#textual.types.NoSelection","title":"NoSelection class","text":"

    Used by the Select widget to flag the unselected state. See Select.BLANK.

    "},{"location":"api/types/#textual.types.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

    Bases: Exception

    Raised when a request has been made for an option that doesn't exist.

    "},{"location":"api/types/#textual.types.RenderStyles","title":"RenderStyles class","text":"
    def __init__(self, node, base, inline_styles):\n

    Bases: StylesBase

    Presents a combined view of two Styles object: a base Styles and inline Styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.base","title":"base property","text":"
    base: Styles\n

    Quick access to base (css) style.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.css","title":"css property","text":"
    css: str\n

    Get the CSS for the combined styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.gutter","title":"gutter property","text":"
    gutter: Spacing\n

    Get space around widget.

    Returns Type Description Spacing

    Space around widget content.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.inline","title":"inline property","text":"
    inline: Styles\n

    Quick access to the inline styles.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.rich_style","title":"rich_style property","text":"
    rich_style: Style\n

    Get a Rich style for this Styles object.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value str | float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation. Defaults to value if not set.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.clear_rule","title":"clear_rule method","text":"
    def clear_rule(self, rule_name):\n

    Clear a rule (from inline).

    "},{"location":"api/types/#textual.css.styles.RenderStyles.get_rules","title":"get_rules method","text":"
    def get_rules(self):\n

    Get rules as a dictionary

    "},{"location":"api/types/#textual.css.styles.RenderStyles.has_rule","title":"has_rule method","text":"
    def has_rule(self, rule):\n

    Check if a rule has been set.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.merge","title":"merge method","text":"
    def merge(self, other):\n

    Merge values from another Styles.

    Parameters Parameter Default Description other StylesBase required

    A Styles object.

    "},{"location":"api/types/#textual.css.styles.RenderStyles.reset","title":"reset method","text":"
    def reset(self):\n

    Reset the rules to initial state.

    "},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter class","text":"

    Helper type for a parameter that isn't specified in a method call.

    "},{"location":"api/validation/","title":"Validation","text":"

    Framework for validating string values

    "},{"location":"api/validation/#textual.validation.Failure","title":"Failure class","text":"

    Information about a validation failure.

    "},{"location":"api/validation/#textual.validation.Failure.description","title":"description class-attribute instance-attribute","text":"
    description: str | None = None\n

    An optional override for describing this failure. Takes precedence over any messages set in the Validator.

    "},{"location":"api/validation/#textual.validation.Failure.validator","title":"validator instance-attribute","text":"
    validator: Validator\n

    The Validator which produced the failure.

    "},{"location":"api/validation/#textual.validation.Failure.value","title":"value class-attribute instance-attribute","text":"
    value: str | None = None\n

    The value which resulted in validation failing.

    "},{"location":"api/validation/#textual.validation.Function","title":"Function class","text":"
    def __init__(self, function, failure_description=None):\n

    Bases: Validator

    A flexible validator which allows you to provide custom validation logic.

    "},{"location":"api/validation/#textual.validation.Function.function","title":"function instance-attribute","text":"
    function = function\n

    Function which takes the value to validate and returns True if valid, and False otherwise.

    "},{"location":"api/validation/#textual.validation.Function.ReturnedFalse","title":"ReturnedFalse class","text":"

    Bases: Failure

    Indicates validation failed because the supplied function returned False.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Function.validate","title":"validate method","text":"
    def validate(self, value):\n

    Validate that the supplied function returns True.

    Parameters Parameter Default Description value str required

    The value to pass into the supplied function.

    Returns Type Description ValidationResult

    A ValidationResult indicating success if the function returned True, and failure if the function return False.

    "},{"location":"api/validation/#textual.validation.Integer","title":"Integer class","text":"

    Bases: Number

    Validator which ensures the value is an integer which falls within a range.

    "},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger class","text":"

    Bases: Failure

    Indicates a failure due to the value not being a valid integer.

    "},{"location":"api/validation/#textual.validation.Integer.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value is an integer, optionally within a range.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Length","title":"Length class","text":"
    def __init__(\n    self,\n    minimum=None,\n    maximum=None,\n    failure_description=None,\n):\n

    Bases: Validator

    Validate that a string is within a range (inclusive).

    "},{"location":"api/validation/#textual.validation.Length.maximum","title":"maximum instance-attribute","text":"
    maximum = maximum\n

    The inclusive maximum length of the value, or None if unbounded.

    "},{"location":"api/validation/#textual.validation.Length.minimum","title":"minimum instance-attribute","text":"
    minimum = minimum\n

    The inclusive minimum length of the value, or None if unbounded.

    "},{"location":"api/validation/#textual.validation.Length.Incorrect","title":"Incorrect class","text":"

    Bases: Failure

    Indicates a failure due to the length of the value being outside the range.

    "},{"location":"api/validation/#textual.validation.Length.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Length.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value falls within the maximum and minimum length constraints.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Number","title":"Number class","text":"
    def __init__(\n    self,\n    minimum=None,\n    maximum=None,\n    failure_description=None,\n):\n

    Bases: Validator

    Validator that ensures the value is a number, with an optional range check.

    "},{"location":"api/validation/#textual.validation.Number.maximum","title":"maximum instance-attribute","text":"
    maximum = maximum\n

    The maximum value of the number, inclusive. If None, the maximum is unbounded.

    "},{"location":"api/validation/#textual.validation.Number.minimum","title":"minimum instance-attribute","text":"
    minimum = minimum\n

    The minimum value of the number, inclusive. If None, the minimum is unbounded.

    "},{"location":"api/validation/#textual.validation.Number.NotANumber","title":"NotANumber class","text":"

    Bases: Failure

    Indicates a failure due to the value not being a valid number (decimal/integer, inc. scientific notation)

    "},{"location":"api/validation/#textual.validation.Number.NotInRange","title":"NotInRange class","text":"

    Bases: Failure

    Indicates a failure due to the number not being within the range [minimum, maximum].

    "},{"location":"api/validation/#textual.validation.Number.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Number.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that value is a valid number, optionally within a range.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Regex","title":"Regex class","text":"
    def __init__(self, regex, flags=0, failure_description=None):\n

    Bases: Validator

    A validator that checks the value matches a regex (via re.fullmatch).

    "},{"location":"api/validation/#textual.validation.Regex.flags","title":"flags instance-attribute","text":"
    flags = flags\n

    The flags to pass to re.fullmatch.

    "},{"location":"api/validation/#textual.validation.Regex.regex","title":"regex instance-attribute","text":"
    regex = regex\n

    The regex which we'll validate is matched by the value.

    "},{"location":"api/validation/#textual.validation.Regex.NoResults","title":"NoResults class","text":"

    Bases: Failure

    Indicates validation failed because the regex could not be found within the value string.

    "},{"location":"api/validation/#textual.validation.Regex.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate method","text":"
    def validate(self, value):\n

    Ensure that the value matches the regex.

    Parameters Parameter Default Description value str required

    The value that should match the regex.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.URL","title":"URL class","text":"

    Bases: Validator

    Validator that checks if a URL is valid (ensuring a scheme is present).

    "},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL class","text":"

    Bases: Failure

    Indicates that the URL is not valid.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Describes why the validator failed.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.URL.validate","title":"validate method","text":"
    def validate(self, value):\n

    Validates that value is a valid URL (contains a scheme).

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult class","text":"

    The result of calling a Validator.validate method.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
    failure_descriptions: list[str]\n

    Utility for extracting failure descriptions as strings.

    Useful if you don't care about the additional metadata included in the Failure objects.

    Returns Type Description list[str]

    A list of the string descriptions explaining the failing validations.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failures","title":"failures class-attribute instance-attribute","text":"
    failures: Sequence[Failure] = field(default_factory=list)\n

    A list of reasons why the value was invalid. Empty if valid=True

    "},{"location":"api/validation/#textual.validation.ValidationResult.is_valid","title":"is_valid property","text":"
    is_valid: bool\n

    True if the validation was successful.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
    def failure(failures):\n

    Construct a failure ValidationResult.

    Parameters Parameter Default Description failures Sequence[Failure] required

    The failures.

    Returns Type Description ValidationResult

    A failure ValidationResult.

    "},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
    def merge(results):\n

    Merge multiple ValidationResult objects into one.

    Parameters Parameter Default Description results Sequence['ValidationResult'] required

    List of ValidationResult objects to merge.

    Returns Type Description 'ValidationResult'

    Merged ValidationResult object.

    "},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
    def success():\n

    Construct a successful ValidationResult.

    Returns Type Description ValidationResult

    A successful ValidationResult.

    "},{"location":"api/validation/#textual.validation.Validator","title":"Validator class","text":"
    def __init__(self, failure_description=None):\n

    Bases: ABC

    Base class for the validation of string values.

    Commonly used in conjunction with the Input widget, which accepts a list of validators via its constructor. This validation framework can also be used to validate any 'stringly-typed' values (for example raw command line input from sys.args).

    To implement your own Validator, subclass this class.

    Example
    class Palindrome(Validator):\n    def validate(self, value: str) -> ValidationResult:\n        def is_palindrome(value: str) -> bool:\n            return value == value[::-1]\n        return self.success() if is_palindrome(value) else self.failure(\"Not palindrome!\")\n
    "},{"location":"api/validation/#textual.validation.Validator.failure_description","title":"failure_description instance-attribute","text":"
    failure_description = failure_description\n

    A description of why the validation failed.

    The description (intended to be user-facing) to attached to the Failure if the validation fails. This failure description is ultimately accessible at the time of validation failure via the Input.Changed or Input.Submitted event, and you can access it on your message handler (a method called, for example, on_input_changed or a method decorated with @on(Input.Changed).

    "},{"location":"api/validation/#textual.validation.Validator.describe_failure","title":"describe_failure method","text":"
    def describe_failure(self, failure):\n

    Return a string description of the Failure.

    Used to provide a more fine-grained description of the failure. A Validator could fail for multiple reasons, so this method could be used to provide a different reason for different types of failure.

    Warning

    This method is only called if no other description has been supplied. If you supply a description inside a call to self.failure(description=\"...\"), or pass a description into the constructor of the validator, those will take priority, and this method won't be called.

    Parameters Parameter Default Description failure Failure required

    Information about why the validation failed.

    Returns Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure method","text":"
    def failure(self, description=None, value=None, failures=None):\n

    Shorthand for signaling validation failure.

    You can return failure(...) from a Validator.validate implementation to signal validation succeeded.

    Parameters Parameter Default Description description str | None None

    The failure description that will be used. When used in conjunction with the Input widget, this is the description that will ultimately be available inside the handler for Input.Changed. If not supplied, the failure_description from the Validator will be used. If that is not supplied either, then the describe_failure method on Validator will be called.

    value str | None None

    The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

    failures Failure | Sequence[Failure] | None None

    The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

    Returns Type Description ValidationResult

    A ValidationResult representing failed validation, and containing the metadata supplied to this function.

    "},{"location":"api/validation/#textual.validation.Validator.success","title":"success method","text":"
    def success(self):\n

    Shorthand for ValidationResult(True).

    You can return success() from a Validator.validate method implementation to signal that validation has succeeded.

    Returns Type Description ValidationResult

    A ValidationResult indicating validation succeeded.

    "},{"location":"api/validation/#textual.validation.Validator.validate","title":"validate abstractmethod","text":"
    def validate(self, value):\n

    Validate the value and return a ValidationResult describing the outcome of the validation.

    Parameters Parameter Default Description value str required

    The value to validate.

    Returns Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/walk/","title":"Walk","text":"

    Functions for walking the DOM.

    Note

    For most purposes you would be better off using query, which uses these functions internally.

    "},{"location":"api/walk/#textual.walk.walk_breadth_first","title":"walk_breadth_first function","text":"
    def walk_breadth_first(\n    root, filter_type=None, *, with_root=True\n):\n

    Walk the tree breadth first (children first).

    Note

    Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

    Parameters Parameter Default Description root DOMNode required

    The root note (starting point).

    filter_type type[WalkType] | None None

    Optional DOMNode subclass to filter by, or None for no filter.

    with_root bool True

    Include the root in the walk.

    Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first function","text":"
    def walk_depth_first(root, filter_type=None, *, with_root=True):\n

    Walk the tree depth first (parents first).

    Note

    Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

    Parameters Parameter Default Description root DOMNode required

    The root note (starting point).

    filter_type type[WalkType] | None None

    Optional DOMNode subclass to filter by, or None for no filter.

    with_root bool True

    Include the root in the walk.

    Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/widget/","title":"Widget","text":"

    The base class for widgets.

    "},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount class","text":"
    def __init__(self, parent, widgets):\n

    An optional awaitable returned by mount and mount_all.

    Example
    await self.mount(Static(\"foo\"))\n
    "},{"location":"api/widget/#textual.widget.MountError","title":"MountError class","text":"

    Bases: WidgetError

    Error raised when there was a problem with the mount request.

    "},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses class","text":"

    Bases: NamedTuple

    Used for render/render_line based widgets that use caching. This structure can be used as a cache-key.

    "},{"location":"api/widget/#textual.widget.PseudoClasses.enabled","title":"enabled instance-attribute","text":"
    enabled: bool\n

    Is 'enabled' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
    focus: bool\n

    Is 'focus' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
    hover: bool\n

    Is 'hover' applied?

    "},{"location":"api/widget/#textual.widget.Widget","title":"Widget class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: DOMNode

    A Widget is the base class for Textual widgets.

    See also static for starting point for your own widgets.

    Parameters Parameter Default Description *children Widget ()

    Child widgets.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
    BORDER_SUBTITLE: str = ''\n

    Initial value for border_subtitle attribute.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
    BORDER_TITLE: str = ''\n

    Initial value for border_title attribute.

    "},{"location":"api/widget/#textual.widget.Widget.allow_horizontal_scroll","title":"allow_horizontal_scroll property","text":"
    allow_horizontal_scroll: bool\n

    Check if horizontal scroll is permitted.

    May be overridden if you want different logic regarding allowing scrolling.

    "},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
    allow_vertical_scroll: bool\n

    Check if vertical scroll is permitted.

    May be overridden if you want different logic regarding allowing scrolling.

    "},{"location":"api/widget/#textual.widget.Widget.auto_links","title":"auto_links class-attribute instance-attribute","text":"
    auto_links: Reactive[bool] = Reactive(True)\n

    Widget will highlight links automatically.

    "},{"location":"api/widget/#textual.widget.Widget.border_subtitle","title":"border_subtitle class-attribute instance-attribute","text":"
    border_subtitle: str | Text | None = _BorderTitle()\n

    A title to show in the bottom border (if there is one).

    "},{"location":"api/widget/#textual.widget.Widget.border_title","title":"border_title class-attribute instance-attribute","text":"
    border_title: str | Text | None = _BorderTitle()\n

    A title to show in the top border (if there is one).

    "},{"location":"api/widget/#textual.widget.Widget.can_focus","title":"can_focus class-attribute instance-attribute","text":"
    can_focus: bool = False\n

    Widget may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.can_focus_children","title":"can_focus_children class-attribute instance-attribute","text":"
    can_focus_children: bool = True\n

    Widget's children may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
    container_size: Size\n

    The size of the container (parent widget).

    Returns Type Description Size

    Container size.

    "},{"location":"api/widget/#textual.widget.Widget.container_viewport","title":"container_viewport property","text":"
    container_viewport: Region\n

    The viewport region (parent window).

    Returns Type Description Region

    The region that contains this widget.

    "},{"location":"api/widget/#textual.widget.Widget.content_offset","title":"content_offset property","text":"
    content_offset: Offset\n

    An offset from the Widget origin where the content begins.

    Returns Type Description Offset

    Offset from widget's origin.

    "},{"location":"api/widget/#textual.widget.Widget.content_region","title":"content_region property","text":"
    content_region: Region\n

    Gets an absolute region containing the content (minus padding and border).

    Returns Type Description Region

    Screen region that contains a widget's content.

    "},{"location":"api/widget/#textual.widget.Widget.content_size","title":"content_size property","text":"
    content_size: Size\n

    The size of the content area.

    Returns Type Description Size

    Content area size.

    "},{"location":"api/widget/#textual.widget.Widget.disabled","title":"disabled class-attribute instance-attribute","text":"
    disabled: Reactive[bool] = disabled\n

    Is the widget disabled? Disabled widgets can not be interacted with, and are typically styled to look dimmer.

    "},{"location":"api/widget/#textual.widget.Widget.dock_gutter","title":"dock_gutter property","text":"
    dock_gutter: Spacing\n

    Space allocated to docks in the parent.

    Returns Type Description Spacing

    Space to be subtracted from scrollable area.

    "},{"location":"api/widget/#textual.widget.Widget.expand","title":"expand class-attribute instance-attribute","text":"
    expand: Reactive[bool] = Reactive(False)\n

    Rich renderable may expand beyond optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
    focusable: bool\n

    Can this widget currently be focused?

    "},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
    gutter: Spacing\n

    Spacing for padding / border / scrollbars.

    Returns Type Description Spacing

    Additional spacing around content area.

    "},{"location":"api/widget/#textual.widget.Widget.has_focus","title":"has_focus class-attribute instance-attribute","text":"
    has_focus: Reactive[bool] = Reactive(False, repaint=False)\n

    Does this widget have focus? Read only.

    "},{"location":"api/widget/#textual.widget.Widget.highlight_link_id","title":"highlight_link_id class-attribute instance-attribute","text":"
    highlight_link_id: Reactive[str] = Reactive('')\n

    The currently highlighted link id. Read only.

    "},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
    horizontal_scrollbar: ScrollBar\n

    The horizontal scrollbar.

    Note

    This will create a scrollbar if one doesn't exist.

    Returns Type Description ScrollBar

    ScrollBar Widget.

    "},{"location":"api/widget/#textual.widget.Widget.hover_style","title":"hover_style class-attribute instance-attribute","text":"
    hover_style: Reactive[Style] = Reactive(\n    Style, repaint=False\n)\n

    The current hover style (style under the mouse cursor). Read only.

    "},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
    is_container: bool\n

    Is this widget a container (contains other widgets)?

    "},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scroll_end","title":"is_horizontal_scroll_end property","text":"
    is_horizontal_scroll_end: bool\n

    Is the horizontal scroll position at the maximum?

    "},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scrollbar_grabbed","title":"is_horizontal_scrollbar_grabbed property","text":"
    is_horizontal_scrollbar_grabbed: bool\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.is_mounted","title":"is_mounted property","text":"
    is_mounted: bool\n

    Check if this widget is mounted.

    "},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable: bool\n

    Can this widget be scrolled?

    "},{"location":"api/widget/#textual.widget.Widget.is_vertical_scroll_end","title":"is_vertical_scroll_end property","text":"
    is_vertical_scroll_end: bool\n

    Is the vertical scroll position at the maximum?

    "},{"location":"api/widget/#textual.widget.Widget.is_vertical_scrollbar_grabbed","title":"is_vertical_scrollbar_grabbed property","text":"
    is_vertical_scrollbar_grabbed: bool\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
    layer: str\n

    Get the name of this widgets layer.

    Returns Type Description str

    Name of layer.

    "},{"location":"api/widget/#textual.widget.Widget.layers","title":"layers property","text":"
    layers: tuple[str, ...]\n

    Layers of from parent.

    Returns Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/widget/#textual.widget.Widget.link_style","title":"link_style property","text":"
    link_style: Style\n

    Style of links.

    Returns Type Description Style

    Rich style.

    "},{"location":"api/widget/#textual.widget.Widget.link_style_hover","title":"link_style_hover property","text":"
    link_style_hover: Style\n

    Style of links underneath the mouse cursor.

    Returns Type Description Style

    Rich Style.

    "},{"location":"api/widget/#textual.widget.Widget.loading","title":"loading class-attribute instance-attribute","text":"
    loading: Reactive[bool] = Reactive(False)\n

    If set to True this widget will temporarily be replaced with a loading indicator.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
    max_scroll_x: int\n

    The maximum value of scroll_x.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_y","title":"max_scroll_y property","text":"
    max_scroll_y: int\n

    The maximum value of scroll_y.

    "},{"location":"api/widget/#textual.widget.Widget.mouse_over","title":"mouse_over class-attribute instance-attribute","text":"
    mouse_over: Reactive[bool] = Reactive(False, repaint=False)\n

    Is the mouse over this widget? Read only.

    "},{"location":"api/widget/#textual.widget.Widget.offset","title":"offset writable property","text":"
    offset: Offset\n

    Widget offset from origin.

    Returns Type Description Offset

    Relative offset.

    "},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
    opacity: float\n

    Total opacity of widget.

    "},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
    outer_size: Size\n

    The size of the widget (including padding and border).

    Returns Type Description Size

    Outer size.

    "},{"location":"api/widget/#textual.widget.Widget.region","title":"region property","text":"
    region: Region\n

    The region occupied by this widget, relative to the Screen.

    Raises Type Description NoScreen

    If there is no screen.

    errors.NoWidget

    If the widget is not on the screen.

    Returns Type Description Region

    Region within screen occupied by widget.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_offset","title":"scroll_offset property","text":"
    scroll_offset: Offset\n

    Get the current scroll offset.

    Returns Type Description Offset

    Offset a container has been scrolled by.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x class-attribute instance-attribute","text":"
    scroll_x: Reactive[float] = Reactive(\n    0.0, repaint=False, layout=False\n)\n

    The scroll position on the X axis.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_y","title":"scroll_y class-attribute instance-attribute","text":"
    scroll_y: Reactive[float] = Reactive(\n    0.0, repaint=False, layout=False\n)\n

    The scroll position on the Y axis.

    "},{"location":"api/widget/#textual.widget.Widget.scrollable_content_region","title":"scrollable_content_region property","text":"
    scrollable_content_region: Region\n

    Gets an absolute region containing the scrollable content (minus padding, border, and scrollbars).

    Returns Type Description Region

    Screen region that contains a widget's content.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
    scrollbar_corner: ScrollBarCorner\n

    The scrollbar corner.

    Note

    This will create a scrollbar corner if one doesn't exist.

    Returns Type Description ScrollBarCorner

    ScrollBarCorner Widget.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_gutter","title":"scrollbar_gutter property","text":"
    scrollbar_gutter: Spacing\n

    Spacing required to fit scrollbar(s).

    Returns Type Description Spacing

    Scrollbar gutter spacing.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_horizontal","title":"scrollbar_size_horizontal property","text":"
    scrollbar_size_horizontal: int\n

    Get the height used by the horizontal scrollbar.

    Returns Type Description int

    Number of rows in the horizontal scrollbar.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_vertical","title":"scrollbar_size_vertical property","text":"
    scrollbar_size_vertical: int\n

    Get the width used by the vertical scrollbar.

    Returns Type Description int

    Number of columns in the vertical scrollbar.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbars_enabled","title":"scrollbars_enabled property","text":"
    scrollbars_enabled: tuple[bool, bool]\n

    A tuple of booleans that indicate if scrollbars are enabled.

    Returns Type Description tuple[bool, bool]

    A tuple of (, )"},{"location":"api/widget/#textual.widget.Widget.scrollbars_space","title":"scrollbars_space property","text":"

    scrollbars_space: tuple[int, int]\n

    The number of cells occupied by scrollbars for width and height

    "},{"location":"api/widget/#textual.widget.Widget.show_horizontal_scrollbar","title":"show_horizontal_scrollbar class-attribute instance-attribute","text":"
    show_horizontal_scrollbar: Reactive[bool] = Reactive(\n    False, layout=True\n)\n

    Show a horizontal scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.show_vertical_scrollbar","title":"show_vertical_scrollbar class-attribute instance-attribute","text":"
    show_vertical_scrollbar: Reactive[bool] = Reactive(\n    False, layout=True\n)\n

    Show a vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink class-attribute instance-attribute","text":"
    shrink: Reactive[bool] = Reactive(True)\n

    Rich renderable may shrink below optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
    siblings: list[Widget]\n

    Get the widget's siblings (self is removed from the return list).

    Returns Type Description list[Widget]

    A list of siblings.

    "},{"location":"api/widget/#textual.widget.Widget.size","title":"size property","text":"
    size: Size\n

    The size of the content area.

    Returns Type Description Size

    Content area size.

    "},{"location":"api/widget/#textual.widget.Widget.tooltip","title":"tooltip writable property","text":"
    tooltip: RenderableType | None\n

    Tooltip for the widget, or None for no tooltip.

    "},{"location":"api/widget/#textual.widget.Widget.vertical_scrollbar","title":"vertical_scrollbar property","text":"
    vertical_scrollbar: ScrollBar\n

    The vertical scrollbar (create if necessary).

    Note

    This will create a scrollbar if one doesn't exist.

    Returns Type Description ScrollBar

    ScrollBar Widget.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_region","title":"virtual_region property","text":"
    virtual_region: Region\n

    The widget region relative to it's container (which may not be visible, depending on scroll offset).

    Returns Type Description Region

    The virtual region.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_region_with_margin","title":"virtual_region_with_margin property","text":"
    virtual_region_with_margin: Region\n

    The widget region relative to its container (including margin), which may not be visible, depending on the scroll offset.

    Returns Type Description Region

    The virtual region of the Widget, inclusive of its margin.

    "},{"location":"api/widget/#textual.widget.Widget.virtual_size","title":"virtual_size class-attribute instance-attribute","text":"
    virtual_size: Reactive[Size] = Reactive(\n    Size(0, 0), layout=True\n)\n

    The virtual (scrollable) size of the widget.

    "},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
    visible_siblings: list[Widget]\n

    A list of siblings which will be shown.

    Returns Type Description list[Widget]

    List of siblings.

    "},{"location":"api/widget/#textual.widget.Widget.window_region","title":"window_region property","text":"
    window_region: Region\n

    The region within the scrollable area that is currently visible.

    Returns Type Description Region

    New region.

    "},{"location":"api/widget/#textual.widget.Widget.allow_focus","title":"allow_focus method","text":"
    def allow_focus(self):\n

    Check if the widget is permitted to focus.

    The base class returns can_focus. This method maybe overridden if additional logic is required.

    Returns Type Description bool

    True if the widget may be focused, or False if it may not be focused.

    "},{"location":"api/widget/#textual.widget.Widget.allow_focus_children","title":"allow_focus_children method","text":"
    def allow_focus_children(self):\n

    Check if a widget's children may be focused.

    The base class returns can_focus_children. This method maybe overridden if additional logic is required.

    Returns Type Description bool

    True if the widget's children may be focused, or False if the widget's children may not be focused.

    "},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate method","text":"
    def animate(\n    self,\n    attribute,\n    value,\n    *,\n    final_value=...,\n    duration=None,\n    speed=None,\n    delay=0.0,\n    easing=DEFAULT_EASING,\n    on_complete=None\n):\n

    Animate an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute to animate.

    value float | Animatable required

    The value to animate to.

    final_value object ...

    The final value of the animation. Defaults to value if not set.

    duration float | None None

    The duration of the animate.

    speed float | None None

    The speed of the animation.

    delay float 0.0

    A delay (in seconds) before the animation starts.

    easing EasingFunction | str DEFAULT_EASING

    An easing method.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print method","text":"
    def begin_capture_print(self, stdout=True, stderr=True):\n

    Capture text from print statements (or writes to stdout / stderr).

    If printing is captured, the widget will be sent an events.Print message.

    Call end_capture_print to disable print capture.

    Parameters Parameter Default Description stdout bool True

    Whether to capture stdout.

    stderr bool True

    Whether to capture stderr.

    "},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur method","text":"
    def blur(self):\n

    Blur (un-focus) the widget.

    Focus will be moved to the next available widget in the focus chain..

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.can_view","title":"can_view method","text":"
    def can_view(self, widget):\n

    Check if a given widget is in the current view (scrollable area).

    Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

    Parameters Parameter Default Description widget Widget required

    A widget that is a descendant of self.

    Returns Type Description bool

    True if the entire widget is in view, False if it is partially visible or not in view.

    "},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse method","text":"
    def capture_mouse(self, capture=True):\n

    Capture (or release) the mouse.

    When captured, mouse events will go to this widget even when the pointer is not directly over the widget.

    Parameters Parameter Default Description capture bool True

    True to capture or False to release.

    "},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled method","text":"
    def check_message_enabled(self, message):\n

    Check if a given message is enabled (allowed to be sent).

    Parameters Parameter Default Description message Message required

    A message object

    Returns Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose method","text":"
    def compose(self):\n

    Called by Textual to create child widgets.

    Extend this to build a UI.

    Example
    def compose(self) -> ComposeResult:\n    yield Header()\n    yield Label(\"Press the button below:\")\n    yield Button()\n    yield Footer()\n
    "},{"location":"api/widget/#textual.widget.Widget.compose_add_child","title":"compose_add_child method","text":"
    def compose_add_child(self, widget):\n

    Add a node to children.

    This is used by the compose process when it adds children. There is no need to use it directly, but you may want to override it in a subclass if you want children to be attached to a different node.

    Parameters Parameter Default Description widget Widget required

    A Widget to add.

    "},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print method","text":"
    def end_capture_print(self):\n

    End print capture (set with begin_capture_print).

    "},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus method","text":"
    def focus(self, scroll_visible=True):\n

    Give focus to this widget.

    Parameters Parameter Default Description scroll_visible bool True

    Scroll parent to make this widget visible.

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id method","text":"
    def get_child_by_id(self, id, expect_type=None):\n

    Return the first child (immediate descendent) of this node with the given ID.

    Parameters Parameter Default Description id str required

    The ID of the child.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type.

    Returns Type Description ExpectType | Widget

    The first child of this node with the ID.

    Raises Type Description NoMatches

    if no children could be found for this ID

    WrongType

    if the wrong type was found.

    "},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type method","text":"
    def get_child_by_type(self, expect_type):\n

    Get the first immediate child of a given type.

    Only returns exact matches, and so will not match subclasses of the given type.

    Parameters Parameter Default Description expect_type type[ExpectType] required

    The type of the child to search for.

    Raises Type Description NoMatches

    If no matching child is found.

    Returns Type Description ExpectType

    The first immediate child widget with the expected type.

    "},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style method","text":"
    def get_component_rich_style(self, name, *, partial=False):\n

    Get a Rich style for a component.

    Parameters Parameter Default Description name str required

    Name of component.

    partial bool False

    Return a partial style (not combined with parent).

    Returns Type Description Style

    A Rich style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height method","text":"
    def get_content_height(self, container, viewport, width):\n

    Called by Textual to get the height of the content area. May be overridden in a subclass.

    Parameters Parameter Default Description container Size required

    Size of the container (immediate parent) widget.

    viewport Size required

    Size of the viewport.

    width int required

    Width of renderable.

    Returns Type Description int

    The height of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width method","text":"
    def get_content_width(self, container, viewport):\n

    Called by textual to get the width of the content area. May be overridden in a subclass.

    Parameters Parameter Default Description container Size required

    Size of the container (immediate parent) widget.

    viewport Size required

    Size of the viewport.

    Returns Type Description int

    The optimal width of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_loading_widget","title":"get_loading_widget method","text":"
    def get_loading_widget(self):\n

    Get a widget to display a loading indicator.

    The default implementation will defer to App.get_loading_widget.

    Returns Type Description Widget

    A widget in place of this widget to indicate a loading.

    "},{"location":"api/widget/#textual.widget.Widget.get_pseudo_class_state","title":"get_pseudo_class_state method","text":"
    def get_pseudo_class_state(self):\n

    Get an object describing whether each pseudo class is present on this object or not.

    Returns Type Description PseudoClasses

    A PseudoClasses object describing the pseudo classes that are present.

    "},{"location":"api/widget/#textual.widget.Widget.get_pseudo_classes","title":"get_pseudo_classes method","text":"
    def get_pseudo_classes(self):\n

    Pseudo classes for a widget.

    Returns Type Description Iterable[str]

    Names of the pseudo classes.

    "},{"location":"api/widget/#textual.widget.Widget.get_style_at","title":"get_style_at method","text":"
    def get_style_at(self, x, y):\n

    Get the Rich style in a widget at a given relative offset.

    Parameters Parameter Default Description x int required

    X coordinate relative to the widget.

    y int required

    Y coordinate relative to the widget.

    Returns Type Description Style

    A rich Style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id method","text":"
    def get_widget_by_id(self, id, expect_type=None):\n

    Return the first descendant widget with the given ID.

    Performs a depth-first search rooted at this widget.

    Parameters Parameter Default Description id str required

    The ID to search for in the subtree.

    expect_type type[ExpectType] | None None

    Require the object be of the supplied type, or None for any type.

    Returns Type Description ExpectType | Widget

    The first descendant encountered with this ID.

    Raises Type Description NoMatches

    if no children could be found for this ID.

    WrongType

    if the wrong type was found.

    "},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount method","text":"
    def mount(self, *widgets, before=None, after=None):\n

    Mount widgets below this widget (making this widget a container).

    Parameters Parameter Default Description *widgets Widget ()

    The widget(s) to mount.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all method","text":"
    def mount_all(self, widgets, *, before=None, after=None):\n

    Mount widgets from an iterable.

    Parameters Parameter Default Description widgets Iterable[Widget] required

    An iterable of widgets.

    before int | str | Widget | None None

    Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

    after int | str | Widget | None None

    Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

    Returns Type Description AwaitMount

    An awaitable object that waits for widgets to be mounted.

    Raises Type Description MountError

    If there is a problem with the mount request.

    Note

    Only one of before or after can be provided. If both are provided a MountError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets","title":"mount_composed_widgets async","text":"
    def mount_composed_widgets(self, widgets):\n

    Called by Textual to mount widgets after compose.

    There is generally no need to implement this method in your application. See Lazy for a class which uses this method to implement lazy mounting.

    Parameters Parameter Default Description widgets list[Widget] required

    A list of child widgets.

    "},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child method","text":"
    def move_child(self, child, *, before=None, after=None):\n

    Move a child widget within its parent's list of children.

    Parameters Parameter Default Description child int | Widget required

    The child widget to move.

    before int | Widget | None None

    Child widget or location index to move before.

    after int | Widget | None None

    Child widget or location index to move after.

    Raises Type Description WidgetError

    If there is a problem with the child or target.

    Note

    Only one of before or after can be provided. If neither or both are provided a WidgetError will be raised.

    "},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify method","text":"
    def notify(\n    self,\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=Notification.timeout\n):\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters Parameter Default Description message str required

    The message for the notification.

    title str ''

    The title for the notification.

    severity SeverityLevel 'information'

    The severity of the notification.

    timeout float Notification.timeout

    The timeout for the notification.

    See App.notify for the full documentation for this method.

    "},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message method","text":"
    def post_message(self, message):\n

    Post a message to this widget.

    Parameters Parameter Default Description message Message required

    Message to post.

    Returns Type Description bool

    True if the message was posted, False if this widget was closed / closing.

    "},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render method","text":"
    def post_render(self, renderable):\n

    Applies style attributes to the default renderable.

    Returns Type Description ConsoleRenderable

    A new renderable.

    "},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh method","text":"
    def refresh(self, *regions, repaint=True, layout=False):\n

    Initiate a refresh of the widget.

    This method sets an internal flag to perform a refresh, which will be done on the next idle event. Only one refresh will be done even if this method is called multiple times.

    By default this method will cause the content of the widget to refresh, but not change its size. You can also set layout=True to perform a layout.

    Warning

    It is rarely necessary to call this method explicitly. Updating styles or reactive attributes will do this automatically.

    Parameters Parameter Default Description *regions Region ()

    Additional screen regions to mark as dirty.

    repaint bool True

    Repaint the widget (will call render() again).

    layout bool False

    Also layout widgets in the view.

    Returns Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse method","text":"
    def release_mouse(self):\n

    Release the mouse.

    Mouse events will only be sent when the mouse is over the widget.

    "},{"location":"api/widget/#textual.widget.Widget.remove","title":"remove method","text":"
    def remove(self):\n

    Remove the Widget from the DOM (effectively deleting it).

    Returns Type Description AwaitRemove

    An awaitable object that waits for the widget to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.remove_children","title":"remove_children method","text":"
    def remove_children(self):\n

    Remove all children of this Widget from the DOM.

    Returns Type Description AwaitRemove

    An awaitable object that waits for the children to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.render","title":"render method","text":"
    def render(self):\n

    Get text or Rich renderable for this widget.

    Implement this for custom widgets.

    Example
    from textual.app import RenderableType\nfrom textual.widget import Widget\n\nclass CustomWidget(Widget):\n    def render(self) -> RenderableType:\n        return \"Welcome to [bold red]Textual[/]!\"\n
    Returns Type Description RenderableType

    Any renderable.

    "},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line method","text":"
    def render_line(self, y):\n

    Render a line of content.

    Parameters Parameter Default Description y int required

    Y Coordinate of line.

    Returns Type Description Strip

    A rendered line.

    "},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines method","text":"
    def render_lines(self, crop):\n

    Render the widget in to lines.

    Parameters Parameter Default Description crop Region required

    Region within visible area to render.

    Returns Type Description list[Strip]

    A list of list of segments.

    "},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str method","text":"
    def render_str(self, text_content):\n

    Convert str in to a Text object.

    If you pass in an existing Text object it will be returned unaltered.

    Parameters Parameter Default Description text_content str | Text required

    Text or str.

    Returns Type Description Text

    A text object.

    "},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
    def run_action(self, action):\n

    Perform a given action, with this widget as the default namespace.

    Parameters Parameter Default Description action str required

    Action encoded as a string.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down method","text":"
    def scroll_down(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one line down.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end method","text":"
    def scroll_end(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to the end of the container.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home method","text":"
    def scroll_home(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to home position.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left method","text":"
    def scroll_left(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one cell left.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down method","text":"
    def scroll_page_down(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page down.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left method","text":"
    def scroll_page_left(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page left.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right method","text":"
    def scroll_page_right(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page right.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up method","text":"
    def scroll_page_up(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one page up.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative method","text":"
    def scroll_relative(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll relative to current position.

    Parameters Parameter Default Description x float | None None

    X distance (columns) to scroll, or None for no change.

    y float | None None

    Y distance (rows) to scroll, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True. Or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right method","text":"
    def scroll_right(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one cell right.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to method","text":"
    def scroll_to(\n    self,\n    x=None,\n    y=None,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters Parameter Default Description x float | None None

    X coordinate (column) to scroll to, or None for no change.

    y float | None None

    Y coordinate (row) to scroll to, or None for no change.

    animate bool True

    Animate to new scroll position.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Note

    The call to scroll is made after the next refresh.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center method","text":"
    def scroll_to_center(\n    self,\n    widget,\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    origin_visible=True,\n    on_complete=None\n):\n

    Scroll this widget to the center of self.

    The center of the widget will be scrolled to the center of the container.

    Parameters Parameter Default Description widget Widget required

    The widget to scroll to the center of self.

    animate bool True

    Whether to animate the scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    origin_visible bool True

    Ensure that the top left corner of the widget remains visible after the scroll.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region method","text":"
    def scroll_to_region(\n    self,\n    region,\n    *,\n    spacing=None,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    center=False,\n    top=False,\n    origin_visible=True,\n    force=False,\n    on_complete=None\n):\n

    Scrolls a given region in to view, if required.

    This method will scroll the least distance required to move region fully within the scrollable area.

    Parameters Parameter Default Description region Region required

    A region that should be visible.

    spacing Spacing | None None

    Optional spacing around the region.

    animate bool True

    True to animate, or False to jump.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    top bool False

    Scroll region to top of container.

    origin_visible bool True

    Ensure that the top left of the widget is within the window.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Returns Type Description Offset

    The distance that was scrolled.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget method","text":"
    def scroll_to_widget(\n    self,\n    widget,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    center=False,\n    top=False,\n    origin_visible=True,\n    force=False,\n    on_complete=None\n):\n

    Scroll scrolling to bring a widget in to view.

    Parameters Parameter Default Description widget Widget required

    A descendant widget.

    animate bool True

    True to animate, or False to jump.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    top bool False

    Scroll widget to top of container.

    origin_visible bool True

    Ensure that the top left of the widget is within the window.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    Returns Type Description bool

    True if any scrolling has occurred in any descendant, otherwise False.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up method","text":"
    def scroll_up(\n    self,\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll one line up.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible method","text":"
    def scroll_visible(\n    self,\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    top=False,\n    easing=None,\n    force=False,\n    on_complete=None\n):\n

    Scroll the container to make this widget visible.

    Parameters Parameter Default Description animate bool True

    Animate scroll.

    speed float | None None

    Speed of scroll if animate is True; or None to use duration.

    duration float | None None

    Duration of animation, if animate is True and speed is None.

    top bool False

    Scroll to top of container.

    easing EasingFunction | str | None None

    An easing method for the scrolling animation.

    force bool False

    Force scrolling even when prohibited by overflow styling.

    on_complete CallbackType | None None

    A callable to invoke when the animation is finished.

    "},{"location":"api/widget/#textual.widget.Widget.set_loading","title":"set_loading method","text":"
    def set_loading(self, loading):\n

    Set or reset the loading state of this widget.

    A widget in a loading state will display a LoadingIndicator that obscures the widget.

    Parameters Parameter Default Description loading bool required

    True to put the widget into a loading state, or False to reset the loading state.

    Returns Type Description Awaitable

    An optional awaitable.

    "},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
    def stop_animation(self, attribute, complete=True):\n

    Stop an animation on an attribute.

    Parameters Parameter Default Description attribute str required

    Name of the attribute whose animation should be stopped.

    complete bool True

    Should the animation be set to its final value?

    Note

    If there is no animation scheduled or running, this is a no-op.

    "},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled method","text":"
    def watch_disabled(self):\n

    Update the styles of the widget and its children when disabled is toggled.

    "},{"location":"api/widget/#textual.widget.Widget.watch_has_focus","title":"watch_has_focus method","text":"
    def watch_has_focus(self, value):\n

    Update from CSS if has focus state changes.

    "},{"location":"api/widget/#textual.widget.Widget.watch_mouse_over","title":"watch_mouse_over method","text":"
    def watch_mouse_over(self, value):\n

    Update from CSS if mouse over state changes.

    "},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError class","text":"

    Bases: Exception

    Base widget error.

    "},{"location":"api/work/","title":"Work","text":"

    A decorator used to create workers.

    Parameters Parameter Default Description method Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None None

    A function or coroutine.

    name str ''

    A short string to identify the worker (in logs and debugging).

    group str 'default'

    A short string to identify a group of workers.

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    exclusive bool False

    Cancel all workers in the same group.

    description str | None None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    thread bool False

    Mark the method as a thread worker.

    "},{"location":"api/worker/","title":"Worker","text":"

    A class to manage concurrent work.

    "},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
    WorkType: TypeAlias = Union[\n    Callable[[], Coroutine[None, None, ResultType]],\n    Callable[[], ResultType],\n    Awaitable[ResultType],\n]\n

    Type used for workers.

    "},{"location":"api/worker/#textual.worker.active_worker","title":"active_worker module-attribute","text":"
    active_worker: ContextVar[Worker] = ContextVar(\n    \"active_worker\"\n)\n

    Currently active worker context var.

    "},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError class","text":"

    Bases: WorkerError

    The operation would result in a deadlock.

    "},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker class","text":"

    Bases: Exception

    There is no active worker.

    "},{"location":"api/worker/#textual.worker.Worker","title":"Worker class","text":"
    def __init__(\n    self,\n    node,\n    work=None,\n    *,\n    name=\"\",\n    group=\"default\",\n    description=\"\",\n    exit_on_error=True,\n    thread=False\n):\n

    Bases: Generic[ResultType]

    A class to manage concurrent work (either a task or a thread).

    Parameters Parameter Default Description node DOMNode required

    The widget, screen, or App that initiated the work.

    work WorkType | None None

    A callable, coroutine, or other awaitable object to run in the worker.

    name str ''

    Name of the worker (short string to help identify when debugging).

    group str 'default'

    The worker group.

    description str ''

    Description of the worker (longer string with more details).

    exit_on_error bool True

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    thread bool False

    Mark the worker as a thread worker.

    "},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
    completed_steps: int\n

    The number of completed steps.

    "},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
    error: BaseException | None\n

    The exception raised by the worker, or None if there was no error.

    "},{"location":"api/worker/#textual.worker.Worker.is_cancelled","title":"is_cancelled property","text":"
    is_cancelled: bool\n

    Has the work been cancelled?

    Note that cancelled work may still be running.

    "},{"location":"api/worker/#textual.worker.Worker.is_finished","title":"is_finished property","text":"
    is_finished: bool\n

    Has the task finished (cancelled, error, or success)?

    "},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
    is_running: bool\n

    Is the task running?

    "},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
    node: DOMNode\n

    The node where this worker was run from.

    "},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
    progress: float\n

    Progress as a percentage.

    If the total steps is None, then this will return 0. The percentage will be clamped between 0 and 100.

    "},{"location":"api/worker/#textual.worker.Worker.result","title":"result property","text":"
    result: ResultType | None\n

    The result of the worker, or None if there is no result.

    "},{"location":"api/worker/#textual.worker.Worker.state","title":"state writable property","text":"
    state: WorkerState\n

    The current state of the worker.

    "},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
    total_steps: int | None\n

    The number of total steps, or None if indeterminate.

    "},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged class","text":"
    def __init__(self, worker, state):\n

    Bases: Message

    The worker state changed.

    Parameters Parameter Default Description worker Worker required

    The worker object.

    state WorkerState required

    New state.

    "},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance method","text":"
    def advance(self, steps=1):\n

    Advance the number of completed steps.

    Parameters Parameter Default Description steps int 1

    Number of steps to advance.

    "},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel method","text":"
    def cancel(self):\n

    Cancel the task.

    "},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
    def run(self):\n

    Run the work.

    Implement this method in a subclass, or pass a callable to the constructor.

    Returns Type Description ResultType

    Return value of the work.

    "},{"location":"api/worker/#textual.worker.Worker.update","title":"update method","text":"
    def update(self, completed_steps=None, total_steps=-1):\n

    Update the number of completed steps.

    Parameters Parameter Default Description completed_steps int | None None

    The number of completed seps, or None to not change.

    total_steps int | None -1

    The total number of steps, None for indeterminate, or -1 to leave unchanged.

    "},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
    def wait(self):\n

    Wait for the work to complete.

    Raises Type Description WorkerFailed

    If the Worker raised an exception.

    WorkerCancelled

    If the Worker was cancelled before it completed.

    Returns Type Description ResultType

    The return value of the work.

    "},{"location":"api/worker/#textual.worker.WorkerCancelled","title":"WorkerCancelled class","text":"

    Bases: WorkerError

    The worker was cancelled and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError class","text":"

    Bases: Exception

    A worker related error.

    "},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed class","text":"
    def __init__(self, error):\n

    Bases: WorkerError

    The worker raised an exception and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState class","text":"

    Bases: enum.Enum

    A description of the worker's current state.

    "},{"location":"api/worker/#textual.worker.WorkerState.CANCELLED","title":"CANCELLED class-attribute instance-attribute","text":"
    CANCELLED = 3\n

    Worker is not running, and was cancelled.

    "},{"location":"api/worker/#textual.worker.WorkerState.ERROR","title":"ERROR class-attribute instance-attribute","text":"
    ERROR = 4\n

    Worker is not running, and exited with an error.

    "},{"location":"api/worker/#textual.worker.WorkerState.PENDING","title":"PENDING class-attribute instance-attribute","text":"
    PENDING = 1\n

    Worker is initialized, but not running.

    "},{"location":"api/worker/#textual.worker.WorkerState.RUNNING","title":"RUNNING class-attribute instance-attribute","text":"
    RUNNING = 2\n

    Worker is running.

    "},{"location":"api/worker/#textual.worker.WorkerState.SUCCESS","title":"SUCCESS class-attribute instance-attribute","text":"
    SUCCESS = 5\n

    Worker is not running, and completed successfully.

    "},{"location":"api/worker/#textual.worker.get_current_worker","title":"get_current_worker function","text":"
    def get_current_worker():\n

    Get the currently active worker.

    Raises Type Description NoActiveWorker

    If there is no active worker.

    Returns Type Description Worker

    A Worker instance.

    "},{"location":"api/worker_manager/","title":"Worker manager","text":"

    A class to manage workers for an app.

    You access this object via App.workers or Widget.workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager","title":"WorkerManager class","text":"
    def __init__(self, app):\n

    An object to manager a number of workers.

    You will not have to construct this class manually, as widgets, screens, and apps have a worker manager accessibly via a workers attribute.

    Parameters Parameter Default Description app App required

    An App instance.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.add_worker","title":"add_worker method","text":"
    def add_worker(self, worker, start=True, exclusive=True):\n

    Add a new worker.

    Parameters Parameter Default Description worker Worker required

    A Worker instance.

    start bool True

    Start the worker if True, otherwise the worker must be started manually.

    exclusive bool True

    Cancel all workers in the same group as worker.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_all","title":"cancel_all method","text":"
    def cancel_all(self):\n

    Cancel all workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_group","title":"cancel_group method","text":"
    def cancel_group(self, node, group):\n

    Cancel a single group.

    Parameters Parameter Default Description node DOMNode required

    Worker DOM node.

    group str required

    A group name.

    Returns Type Description list[Worker]

    A list of workers that were cancelled.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_node","title":"cancel_node method","text":"
    def cancel_node(self, node):\n

    Cancel all workers associated with a given node

    Parameters Parameter Default Description node DOMNode required

    A DOM node (widget, screen, or App).

    Returns Type Description list[Worker]

    List of cancelled workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.start_all","title":"start_all method","text":"
    def start_all(self):\n

    Start all the workers.

    "},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
    def wait_for_complete(self, workers=None):\n

    Wait for workers to complete.

    Parameters Parameter Default Description workers Iterable[Worker] | None None

    An iterable of workers or None to wait for all workers in the manager.

    "},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2023/03/15/no-async-async-with-python/","title":"No-async async with Python","text":"

    A (reasonable) criticism of async is that it tends to proliferate in your code. In order to await something, your functions must be async all the way up the call-stack. This tends to result in you making things async just to support that one call that needs it or, worse, adding async just-in-case. Given that going from def to async def is a breaking change there is a strong incentive to go straight there.

    Before you know it, you have adopted a policy of \"async all the things\".

    Textual is an async framework, but doesn't require the app developer to use the async and await keywords (but you can if you need to). This post is about how Textual accomplishes this async-agnosticism.

    Info

    See this example from the docs for an async-less Textual app.

    "},{"location":"blog/2023/03/15/no-async-async-with-python/#an-apology","title":"An apology","text":"

    But first, an apology! In a previous post I said Textual \"doesn't do any IO of its own\". This is not accurate. Textual responds to keys and mouse events (Input) and writes content to the terminal (Output).

    Although Textual clearly does do IO, it uses asyncio mainly for concurrency. It allows each widget to update its part of the screen independently from the rest of the app.

    "},{"location":"blog/2023/03/15/no-async-async-with-python/#await-me-maybe","title":"Await me (maybe)","text":"

    The first no-async async technique is the \"Await me maybe\" pattern, a term first coined by Simon Willison. This is particularly applicable to callbacks (or in Textual terms, message handlers).

    The await_me_maybe function below can run a callback that is either a plain old function or a coroutine (async def). It does this by awaiting the result of the callback if it is awaitable, or simply returning the result if it is not.

    import asyncio\nimport inspect\n\n\ndef plain_old_function():\n    return \"Plain old function\"\n\nasync def async_function():\n    return \"Async function\"\n\n\nasync def await_me_maybe(callback):\n    result = callback()\n    if inspect.isawaitable(result):\n        return await result\n    return result\n\n\nasync def run_framework():\n    print(\n        await await_me_maybe(plain_old_function)\n    )\n    print(\n        await await_me_maybe(async_function)\n    )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run_framework())\n
    "},{"location":"blog/2023/03/15/no-async-async-with-python/#optionally-awaitable","title":"Optionally awaitable","text":"

    The \"await me maybe\" pattern is great when an async framework calls the app's code. The app developer can choose to write async code or not. Things get a little more complicated when the app wants to call the framework's API. If the API has asynced all the things, then it would force the app to do the same.

    Textual's API consists of regular methods for the most part, but there are a few methods which are optionally awaitable. These are not coroutines (which must be awaited to do anything).

    In practice, this means that those API calls initiate something which will complete a short time later. If you discard the return value then it won't prevent it from working. You only need to await if you want to know when it has finished.

    The mount method is one such method. Calling it will add a widget to the screen:

    def on_key(self):\n    # Add MyWidget to the screen\n    self.mount(MyWidget(\"Hello, World!\"))\n

    In this example we don't care that the widget hasn't been mounted immediately, only that it will be soon.

    Note

    Textual awaits the result of mount after the message handler, so even if you don't explicitly await it, it will have been completed by the time the next message handler runs.

    We might care if we want to mount a widget then make some changes to it. By making the handler async and awaiting the result of mount, we can be sure that the widget has been initialized before we update it:

    async def on_key(self):\n    # Add MyWidget to the screen\n    await self.mount(MyWidget(\"Hello, World!\"))\n    # add a border\n    self.query_one(MyWidget).styles.border = (\"heavy\", \"red\")\n

    Incidentally, I found there were very few examples of writing awaitable objects in Python. So here is the code for AwaitMount which is returned by the mount method:

    class AwaitMount:\n    \"\"\"An awaitable returned by mount() and mount_all().\"\"\"\n\n    def __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:\n        self._parent = parent\n        self._widgets = widgets\n\n    async def __call__(self) -> None:\n        \"\"\"Allows awaiting via a call operation.\"\"\"\n        await self\n\n    def __await__(self) -> Generator[None, None, None]:\n        async def await_mount() -> None:\n            if self._widgets:\n                aws = [\n                    create_task(widget._mounted_event.wait(), name=\"await mount\")\n                    for widget in self._widgets\n                ]\n                if aws:\n                    await wait(aws)\n                    self._parent.refresh(layout=True)\n\n        return await_mount().__await__()\n
    "},{"location":"blog/2023/03/15/no-async-async-with-python/#summing-up","title":"Summing up","text":"

    Textual did initially \"async all the things\", which you might see if you find some old Textual code. Now async is optional.

    This is not because I dislike async. I'm a fan! But it does place a small burden on the developer (more to type and think about). With the current API you generally don't need to write coroutines, or remember to await things. But async is there if you need it.

    We're finding that Textual is increasingly becoming a UI to things which are naturally concurrent, so async was a good move. Concurrency can be a tricky subject, so we're planning some API magic to take the pain out of running tasks, threads, and processes. Stay tuned!

    Join us on our Discord server if you want to talk about these things with the Textualize developers.

    "},{"location":"blog/2022/12/08/be-the-keymaster/","title":"Be the Keymaster!","text":""},{"location":"blog/2022/12/08/be-the-keymaster/#that-didnt-go-to-plan","title":"That didn't go to plan","text":"

    So... yeah... the blog. When I wrote my previous (and first) post I had wanted to try and do a post towards the end of each week, highlighting what I'd done on the \"dogfooding\" front. Life kinda had other plans. Not in a terrible way, but it turns out that getting both flu and Covid jabs (AKA \"jags\" as they tend to say in my adopted home) on the same day doesn't really agree with me too well.

    I have been working, but there's been some odd moments in the past week and a bit and, last week, once I got to the end, I was glad for it to end. So no blog post happened.

    Anyway...

    "},{"location":"blog/2022/12/08/be-the-keymaster/#what-have-i-been-up-to","title":"What have I been up to?","text":"

    While mostly sat feeling sorry for myself on my sofa, I have been coding. Rather than list all the different things here in detail, I'll quickly mention them with links to where to find them and play with them if you want:

    "},{"location":"blog/2022/12/08/be-the-keymaster/#fivepyfive","title":"FivePyFive","text":"

    While my Textual 5x5 puzzle is one of the examples in the Textual repo, I wanted to make it more widely available so people can download it with pip or pipx. See over on PyPi and see if you can solve it. ;-)

    "},{"location":"blog/2022/12/08/be-the-keymaster/#textual-qrcode","title":"textual-qrcode","text":"

    I wanted to put together a very small example of how someone may put together a third party widget library, and in doing so selected what I thought was going to be a mostly-useless example: a wrapper around a text-based QR code generator website. Weirdly I've had a couple of people express a need for QR codes in the terminal since publishing that!

    "},{"location":"blog/2022/12/08/be-the-keymaster/#pispy","title":"PISpy","text":"

    PISpy is a very simple terminal-based client for the PyPi API. Mostly it provides a hypertext interface to Python package details, letting you look up a package and then follow its dependency links. It's very simple at the moment, but I think more fun things can be done with this.

    "},{"location":"blog/2022/12/08/be-the-keymaster/#oidia","title":"OIDIA","text":"

    I'm a big fan of the use of streak-tracking in one form or another. Personally I use a streak-tracking app for keeping tabs of all sorts of good (and bad) habits, and as a heavy user of all things Apple I make a lot of use of the Fitness rings, etc. So I got to thinking it might be fun to do a really simple, no shaming, no counting, just recording, steak app for the Terminal. OIDIA is the result.

    As of the time of writing I only finished the first version of this yesterday evening, so there are plenty of rough edges; but having got it to a point where it performed the basic tasks I wanted from it, that seemed like a good time to publish.

    Expect to see this getting more updates and polish.

    "},{"location":"blog/2022/12/08/be-the-keymaster/#wait-what-about-this-keymaster-thing","title":"Wait, what about this Keymaster thing?","text":"

    Ahh, yes, about that... So one of the handy things I'm finding about Textual is its key binding system. The more I build Textual apps, the more I appreciate the bindings, how they can be associated with specific widgets, the use of actions (which can be used from other places too), etc.

    But... (there's always a \"but\" right -- I mean, there'd be no blog post to be had here otherwise).

    The terminal doesn't have access to all the key combinations you may want to use, and also, because some keys can't necessarily be \"typed\", at least not easily (think about it: there's no F1 character, you have to type F1), many keys and key combinations need to be bound with specific names.

    So there's two problems here: how do I discover what keys even turn up in my application, and when they do, what should I call them when I pass them to Binding?

    That felt like a \"well Dave just build an app for it!\" problem. So I did:

    If you're building apps with Textual and you want to discover what keys turn up from your terminal and are available to your application, you can:

    $ pipx install textual-keys\n

    and then just run textual-keys and start mashing the keyboard to find out.

    There's a good chance that this app, or at least a version of it, will make it into Textual itself (very likely as one of the devtools). But for now it's just an easy install away.

    I think there's a call to be made here too: have you built anything to help speed up how you work with Textual, or just make the development experience \"just so\"? If so, do let us know, and come yell about it on the #show-and-tell channel in our Discord server.

    "},{"location":"blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/","title":"A better asyncio sleep for Windows to fix animation","text":"

    I spent some time optimizing Textual on Windows recently, and discovered something which may be of interest to anyone working with async code on that platform.

    Animation, scrolling, and fading had always been unsatisfactory on Windows. Textual was usable, but the lag when scrolling made apps feel far less snappy that other platforms. On macOS and Linux, scrolling is fast enough that it feels close to a native app, not something running in a terminal. Yet the Windows experience never improved, even as Textual got faster with each release.

    I had chalked this up to Windows Terminal being slow to render updates. After all, the classic Windows terminal was (and still is) glacially slow. Perhaps Microsoft just weren't focusing on performance.

    In retrospect, that was highly improbable. Like all modern terminals, Windows Terminal uses the GPU to render updates. Even without focussing on performance, it should be fast.

    I figured I'd give it one last attempt to speed up Textual on Windows. If I failed, Windows would forever be a third-class platform for Textual apps.

    It turned out that it was nothing to do with performance, per se. The issue was with a single asyncio function: asyncio.sleep.

    Textual has a Timer class which creates events at regular intervals. It powers the JS-like set_interval and set_timer functions. It is also used internally to do animation (such as smooth scrolling). This Timer class calls asyncio.sleep to wait the time between one event and the next.

    On macOS and Linux, calling asynco.sleep is fairly accurate. If you call sleep(3.14), it will return within 1% of 3.14 seconds. This is not the case for Windows, which for historical reasons uses a timer with a granularity of 15 milliseconds. The upshot is that sleep times will be rounded up to the nearest multiple of 15 milliseconds.

    This limit appears to hold true for all async primitives on Windows. If you wait for something with a timeout, it will return on a multiple of 15 milliseconds. Fortunately there is work in the CPython pipeline to make this more accurate. Thanks to Steve Dower for pointing this out.

    This lack of accuracy in the timer meant that timer events were created at a far slower rate than intended. Animation was slower because Textual was waiting too long between updates.

    Once I had figured that out, I needed an alternative to asyncio.sleep for Textual's Timer class. And I found one. The following version of sleep is accurate to well within 1%:

    from time import sleep as time_sleep\nfrom asyncio import get_running_loop\n\nasync def sleep(sleep_for: float) -> None:\n    \"\"\"An asyncio sleep.\n\n    On Windows this achieves a better granularity than asyncio.sleep\n\n    Args:\n        sleep_for (float): Seconds to sleep for.\n    \"\"\"    \n    await get_running_loop().run_in_executor(None, time_sleep, sleep_for)\n

    That is a drop-in replacement for sleep on Windows. With it, Textual runs a lot smoother. Easily on par with macOS and Linux.

    It's not quite perfect. There is a little tearing during full \"screen\" updates, but performance is decent all round. I suspect when this bug is fixed (big thanks to Paul Moore for looking in to that), and Microsoft implements this protocol then Textual on Windows will be A+.

    This Windows improvement will be in v0.9.0 of Textual, which will be released in a few days.

    "},{"location":"blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/","title":"The Heisenbug lurking in your async code","text":"

    I'm taking a brief break from blogging about Textual to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this tweet.

    If you have ever used asyncio.create_task you may have created a bug for yourself that is challenging (read almost impossible) to reproduce. If it occurs, your code will likely fail in unpredictable ways.

    The root cause of this Heisenbug is that if you don't hold a reference to the task object returned by create_task then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.

    This behavior is well documented, as you can see from this excerpt (emphasis mine):

    But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a lot of projects where this bug is waiting for just the right moment to ruin somebody's day.

    I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as \"daemon\" threads they will exist for the lifetime of your app. Not so with Tasks.

    The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use TaskGroups which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine.

    "},{"location":"blog/2023/03/08/overhead-of-python-asyncio-tasks/","title":"Overhead of Python Asyncio tasks","text":"

    Every widget in Textual, be it a button, tree view, or a text input, runs an asyncio task. There is even a task for scrollbar corners (the little space formed when horizontal and vertical scrollbars meet).

    Info

    It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. Those tasks are used to power message queues, so that widgets (UI components) can do whatever they do at their own pace.

    Its fair to say that Textual apps launch a lot of tasks. Which is why when I was trying to optimize startup (for apps with 1000s of widgets) I suspected it was task related.

    I needed to know how much of an overhead it was to launch tasks. Tasks are lighter weight than threads, but how much lighter? The only way to know for certain was to profile.

    The following code launches a load of do nothing tasks, then waits for them to shut down. This would give me an idea of how performant create_task is, and also a baseline for optimizations. I would know the absolute limit of any optimizations I make.

    from asyncio import create_task, wait, run\nfrom time import process_time as time\n\n\nasync def time_tasks(count=100) -> float:\n    \"\"\"Time creating and destroying tasks.\"\"\"\n\n    async def nop_task() -> None:\n        \"\"\"Do nothing task.\"\"\"\n        pass\n\n    start = time()\n    tasks = [create_task(nop_task()) for _ in range(count)]\n    await wait(tasks)\n    elapsed = time() - start\n    return elapsed\n\n\nfor count in range(100_000, 1000_000 + 1, 100_000):\n    create_time = run(time_tasks(count))\n    create_per_second = 1 / (create_time / count)\n    print(f\"{count:,} tasks \\t {create_per_second:0,.0f} tasks per/s\")\n

    And here is the output:

    100,000 tasks    280,003 tasks per/s\n200,000 tasks    255,275 tasks per/s\n300,000 tasks    248,713 tasks per/s\n400,000 tasks    248,383 tasks per/s\n500,000 tasks    241,624 tasks per/s\n600,000 tasks    260,660 tasks per/s\n700,000 tasks    244,510 tasks per/s\n800,000 tasks    247,455 tasks per/s\n900,000 tasks    242,744 tasks per/s\n1,000,000 tasks          259,715 tasks per/s\n

    Info

    Running on an M1 MacBook Pro.

    This tells me I can create, run, and shutdown 260K tasks per second.

    That's fast.

    Clearly create_task is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). I've noticed some big wins there, so the next version of Textual will be faster to start apps with a metric tonne of widgets.

    But I still need to know what to do with those scrollbar corners. A task for two characters. I don't even...

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/","title":"A year of building for the terminal","text":"

    I joined Textualize back in January 2022, and since then have been hard at work with the team on both Rich and Textual. Over the course of the year, I\u2019ve been able to work on a lot of really cool things. In this post, I\u2019ll review a subset of the more interesting and visual stuff I\u2019ve built. If you\u2019re into terminals and command line tooling, you\u2019ll hopefully see at least one thing of interest!

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#a-file-manager-powered-by-textual","title":"A file manager powered by Textual","text":"

    I\u2019ve been slowly developing a file manager as a \u201cdogfooding\u201d project for Textual. It takes inspiration from tools such as Ranger and Midnight Commander.

    As of December 2022, it lets you browse your file system, filtering, multi-selection, creating and deleting files/directories, opening files in your $EDITOR and more.

    I\u2019m happy with how far this project has come \u2014 I think it\u2019s a good example of the type of powerful application that can be built with Textual with relatively little code. I\u2019ve been able to focus on features, instead of worrying about terminal emulator implementation details.

    The project is available on GitHub.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#better-diffs-in-the-terminal","title":"Better diffs in the terminal","text":"

    Diffs in the terminal are often difficult to read at a glance. I wanted to see how close I could get to achieving a diff display of a quality similar to that found in the GitHub UI.

    To attempt this, I built a tool called Dunk. It\u2019s a command line program which you can pipe your git diff output into, and it\u2019ll convert it into something which I find much more readable.

    Although I\u2019m not particularly proud of the code - there are a lot of \u201chacks\u201d going on, but I\u2019m proud of the result. If anything, it shows what can be achieved for tools like this.

    For many diffs, the difference between running git diff and git diff | dunk | less -R is night and day.

    It\u2019d be interesting to revisit this at some point. It has its issues, but I\u2019d love to see how it can be used alongside Textual to build a terminal-based diff/merge tool. Perhaps it could be combined with\u2026

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#code-editor-floating-gutter","title":"Code editor floating gutter","text":"

    This is a common feature in text editors and IDEs: when you scroll to the right, you should still be able to see what line you\u2019re on. Out of interest, I tried to recreate the effect in the terminal using Textual.

    Textual CSS offers a dock property which allows you to attach a widget to an edge of its parent. By creating a widget that contains a vertical list of numbers and setting the dock property to left, we can create a floating gutter effect. Then, we just need to keep the scroll_y in sync between the gutter and the content to ensure the line numbers stay aligned.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#dropdown-autocompletion-menu","title":"Dropdown autocompletion menu","text":"

    While working on Shira (a proof-of-concept, terminal-based Python object explorer), I wrote some autocompleting dropdown functionality.

    Textual forgoes the z-index concept from browser CSS and instead uses a \u201cnamed layer\u201d system. Using the layers property you can defined an ordered list of named layers, and using the layer property, you can assign a descendant widget to one of those layers.

    By creating a new layer above all others and assigning a widget to that layer, we can ensure that widget is painted above everything else.

    In order to determine where to place the dropdown, we can track the current value in the dropdown by watching the reactive input \u201cvalue\u201d inside the Input widget. This method will be called every time the value of the Input changes, and we can use this hook to amend the position of our dropdown position to accommodate for the length of the input value.

    I\u2019ve now extracted this into a separate library called textual-autocomplete.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#tabs-with-animated-underline","title":"Tabs with animated underline","text":"

    The aim here was to create a tab widget with underlines that animates smoothly as another tab is selected.

    The difficulty with implementing something like this is that we don\u2019t have pixel-perfect resolution when animating - a terminal window is just a big grid of fixed-width character cells.

    However, when animating things in a terminal, we can often achieve better granularity using Unicode related tricks. In this case, instead of shifting the bar along one whole cell, we adjust the endings of the bar to be a character which takes up half of a cell.

    The exact characters that form the bar are \"\u257a\", \"\u2501\" and \"\u2578\". When the bar sits perfectly within cell boundaries, every character is \u201c\u2501\u201d. As it travels over a cell boundary, the left and right ends of the bar are updated to \"\u257a\" and \"\u2578\" respectively.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#snapshot-testing-for-terminal-apps","title":"Snapshot testing for terminal apps","text":"

    One of the great features we added to Rich this year was the ability to export console contents to an SVG. This feature was later exposed to Textual, allowing users to capture screenshots of their running Textual apps. Ultimately, I ended up creating a tool for snapshot testing in the Textual codebase.

    Snapshot testing is used to ensure that Textual output doesn\u2019t unexpectedly change. On disk, we store what we expect the output to look like. Then, when we run our unit tests, we get immediately alerted if the output has changed.

    This essentially automates the process of manually spinning up several apps and inspecting them for unexpected visual changes. It\u2019s great for catching subtle regressions!

    In Textual, each CSS property has its own canonical example and an associated snapshot test. If we accidentally break a property in a way that affects the visual output, the chances of it sneaking into a release are greatly reduced, because the corresponding snapshot test will fail.

    As part of this work, I built a web interface for comparing snapshots with test output. There\u2019s even a little toggle which highlights the differences, since they\u2019re sometimes rather subtle.

    Since the terminal output shown in the video above is just an SVG image, I was able to add the \"Show difference\" functionality by overlaying the two images and applying a single CSS property: mix-blend-mode: difference;.

    The snapshot testing functionality itself is implemented as a pytest plugin, and it builds on top of a snapshot testing framework called syrupy.

    It's quite likely that this will eventually be exposed to end-users of Textual.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#demonstrating-animation","title":"Demonstrating animation","text":"

    I built an example app to demonstrate how to animate in Textual and the available easing functions.

    The smoothness here is achieved using tricks similar to those used in the tabs I discussed earlier. In fact, the bar that animates in the video above is the same Rich renderable that is used by Textual's scrollbars.

    You can play with this app by running textual easing. Please use animation sparingly.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#developer-console","title":"Developer console","text":"

    When developing terminal based applications, performing simple debugging using print can be difficult, since the terminal is in application mode.

    A project I worked on earlier in the year to improve the situation was the Textual developer console, which you can launch with textual console.

    On the right, Dave's 5x5 Textual app. On the left, the Textual console.

    Then, by running a Textual application with the --dev flag, all standard output will be redirected to it. This means you can use the builtin print function and still immediately see the output. Textual itself also writes information to this console, giving insight into the messages that are flowing through an application.

    "},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#pixel-art","title":"Pixel art","text":"

    Cells in the terminal are roughly two times taller than they are wide. This means, that two horizontally adjacent cells form an approximate square.

    Using this fact, I wrote a simple library based on Rich and PIL which can convert an image file into terminal output. You can find the library, rich-pixels, on GitHub.

    It\u2019s particularly good for displaying simple pixel art images. The SVG image below is also a good example of the SVG export functionality I touched on earlier.

    Rich

    Since the library generates an object which is renderable using Rich, these can easily be embedded inside Textual applications.

    Here's an example of that in a scrapped \"Pok\u00e9dex\" app I threw together:

    This is a rather naive approach to the problem... but I did it for fun!

    Other methods for displaying images in the terminal include:

    • A more advanced library like chafa, which uses a range of Unicode characters to achieve a more accurate representation of the image.
    • One of the available terminal image protocols, such as Sixel, Kitty\u2019s Terminal Graphics Protocol, and iTerm Inline Images Protocol.

    That was a whirlwind tour of just some of the projects I tackled in 2022. If you found it interesting, be sure to follow me on Twitter. I don't post often, but when I do, it's usually about things similar to those I've discussed here.

    "},{"location":"blog/2022/11/06/new-blog/","title":"New Blog","text":"

    Welcome to the first post on the Textual blog.

    I plan on using this as a place to make announcements regarding new releases of Textual, and any other relevant news.

    The first piece of news is that we've reorganized this site a little. The Events, Styles, and Widgets references are now under \"Reference\", and what used to be under \"Reference\" is now \"API\" which contains API-level documentation. I hope that's a little clearer than it used to be!

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/","title":"So you're looking for a wee bit of Textual help...","text":""},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#introduction","title":"Introduction","text":"

    Quote

    Patience, Highlander. You have done well. But it'll take time. You are generations being born and dying. You are at one with all living things. Each man's thoughts and dreams are yours to know. You have power beyond imagination. Use it well, my friend. Don't lose your head.

    Juan S\u00e1nchez Villalobos Ram\u00edrez, Chief metallurgist to King Charles V of Spain

    As of the time of writing, I'm a couple or so days off having been with Textualize for 3 months. It's been fun, and educational, and every bit as engaging as I'd hoped, and more. One thing I hadn't quite prepared for though, but which I really love, is how so many other people are learning Textual along with me.

    Even in those three months the library has changed and expanded quite a lot, and it continues to do so. Meanwhile, more people are turning up and using the framework; you can see this online in social media, blogs and of course in the ever-growing list of projects on GitHub which depend on Textual.

    This inevitably means there's a lot of people getting to grips with a new tool, and one that is still a bit of a moving target. This in turn means lots of people are coming to us to get help.

    As I've watched this happen I've noticed a few patterns emerging. Some of these good or neutral, some... let's just say not really beneficial to those seeking the help, or to those trying to provide the help. So I wanted to write a little bit about the different ways you can get help with Textual and your Textual-based projects, and to also try and encourage people to take the most helpful and positive approach to getting that help.

    Now, before I go on, I want to make something very clear: I'm writing this as an individual. This is my own personal view, and my own advice from me to anyone who wishes to take it. It's not Textual (the project) or Textualize (the company) policy, rules or guidelines. This is just some ageing hacker's take on how best to go about asking for help, informed by years of asking for and also providing help in email, on Usenet, on forums, etc.

    Or, put another way: if what you read in here seems sensible to you, I figure we'll likely have already hit it off over on GitHub or in the Discord server. ;-)

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#where-to-go-for-help","title":"Where to go for help","text":"

    At this point this is almost a bit of an FAQ itself, so I thought I'd address it here: where's the best place to ask for help about Textual, and what's the difference between GitHub Issues, Discussions and our Discord server?

    I'd suggest thinking of them like this:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#discord","title":"Discord","text":"

    You have a question, or need help with something, and perhaps you could do with a reply as soon as possible. But, and this is the really important part, it doesn't matter if you don't get a response. If you're in this situation then the Discord server is possibly a good place to start. If you're lucky someone will be hanging about who can help out.

    I can't speak for anyone else, but keep this in mind: when I look in on Discord I tend not to go scrolling back much to see if anything has been missed. If something catches my eye, I'll try and reply, but if it doesn't... well, it's mostly an instant chat thing so I don't dive too deeply back in time.

    Going from Discord to a GitHub issue

    As a slight aside here: sometimes people will pop up in Discord, ask a question about something that turns out looking like a bug, and that's the last we hear of it. Please, please, please, if this happens, the most helpful thing you can do is go raise an issue for us. It'll help us to keep track of problems, it'll help get your problem fixed, it'll mean everyone benefits.

    My own advice would be to treat Discord as an ephemeral resource. It happens in the moment but fades away pretty quickly. It's like knocking on a friend's door to see if they're in. If they're not in, you might leave them a note, which is sort of like going to...

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#github","title":"GitHub","text":"

    On the other hand, if you have a question or need some help or something where you want to stand a good chance of the Textual developers (amongst others) seeing it and responding, I'd recommend that GitHub is the place to go. Dropping something into the discussions there, or leaving an issue, ensures it'll get seen. It won't get lost.

    As for which you should use -- a discussion or an issue -- I'd suggest this: if you need help with something, or you want to check your understanding of something, or you just want to be sure something is a problem before taking it further, a discussion might be the best thing. On the other hand, if you've got a clear bug or feature request on your hands, an issue makes a lot of sense.

    Don't worry if you're not sure which camp your question or whatever falls into though; go with what you think is right. There's no harm done either way (I may move an issue to a discussion first before replying, if it's really just a request for help -- but that's mostly so everyone can benefit from finding it in the right place later on down the line).

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#the-dos-and-donts-of-getting-help","title":"The dos and don'ts of getting help","text":"

    Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A little bit. Again, please remember, this isn't a set of rules, this isn't a set of official guidelines, this is just a bunch of \"if you want my advice, and I know you didn't ask but you've read this far so you actually sort of did don't say I didn't warn you!\" waffle.

    This isn't going to be an exhaustive collection, far from it. But I feel these are some important highlights.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#do","title":"Do...","text":"

    When looking for help, in any of the locations mentioned above, I'd totally encourage:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-clear-and-detailed","title":"Be clear and detailed","text":"

    Too much detail is almost always way better than not enough. \"My program didn't run\", often even with some of the code supplied, is so much harder to help than \"I ran this code I'm posting here, and I expected this particular outcome, and I expected it because I'd read this particular thing in the docs and had comprehended it to mean this, but instead the outcome was this exception here, and I'm a bit stuck -- can someone offer some pointers?\"

    The former approach means there often ends up having to be a back and forth which can last a long time, and which can sometimes be frustrating for the person asking. Manage frustration: be clear, tell us everything you can.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#say-what-resources-youve-used-already","title":"Say what resources you've used already","text":"

    If you've read the potions of the documentation that relate to what you're trying to do, it's going to be really helpful if you say so. If you don't, it might be assumed you haven't and you may end up being pointed at them.

    So, please, if you've checked the documentation, looked in the FAQ, done a search of past issues or discussions or perhaps even done a search on the Discord server... please say so.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-polite","title":"Be polite","text":"

    This one can go a long way when looking for help. Look, I get it, programming is bloody frustrating at times. We've all rage-quit some code at some point, I'm sure. It's likely going to be your moment of greatest frustration when you go looking for help. But if you turn up looking for help acting all grumpy and stuff it's not going to come over well. Folk are less likely to be motivated to lend a hand to someone who seems rather annoyed.

    If you throw in a please and thank-you here and there that makes it all the better.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#fully-consider-the-replies","title":"Fully consider the replies","text":"

    You could find yourself getting a reply that you're sure won't help at all. That's fair. But be sure to fully consider it first. Perhaps you missed the obvious along the way and this is 100% the course correction you'd unknowingly come looking for in the first place. Sure, the person replying might have totally misunderstood what was being asked, or might be giving a wrong answer (it me! I've totally done that and will again!), but even then a reply along the lines of \"I'm not sure that's what I'm looking for, because...\" gets everyone to the solution faster than \"lol nah\".

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#entertain-what-might-seem-like-odd-questions","title":"Entertain what might seem like odd questions","text":"

    Aye, I get it, being asked questions when you're looking for an answer can be a bit frustrating. But if you find yourself on the receiving end of a small series of questions about your question, keep this in mind: Textual is still rather new and still developing and it's possible that what you're trying to do isn't the correct way to do that thing. To the person looking to help you it may seem to them you have an XY problem.

    Entertaining those questions might just get you to the real solution to your problem.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#allow-for-language-differences","title":"Allow for language differences","text":"

    You don't need me to tell you that a project such as Textual has a global audience. With that rather obvious fact comes the other fact that we don't all share the same first language. So, please, as much as possible, try and allow for that. If someone is trying to help you out, and they make it clear they're struggling to follow you, keep this in mind.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#acknowledge-the-answer","title":"Acknowledge the answer","text":"

    I suppose this is a variation on \"be polite\" (really, a thanks can go a long way), but there's more to this than a friendly acknowledgement. If someone has gone to the trouble of offering some help, it's helpful to everyone who comes after you to acknowledge if it worked or not. That way a future help-seeker will know if the answer they're reading stands a chance of being the right one.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#accept-that-textual-is-zero-point-software-right-now","title":"Accept that Textual is zero-point software (right now)","text":"

    Of course the aim is to have every release of Textual be stable and useful, but things will break. So, please, do keep in mind things like:

    • Textual likely doesn't have your feature of choice just yet.
    • We might accidentally break something (perhaps pinning Textual and testing each release is a good plan here?).
    • We might deliberately break something because we've decided to take a particular feature or way of doing things in a better direction.

    Of course it can be a bit frustrating a times, but overall the aim is to have the best framework possible in the long run.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#dont","title":"Don't...","text":"

    Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd personally discourage:

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#lack-patience","title":"Lack patience","text":"

    Sure, it can be annoying. You're in your flow, you've got a neat idea for a thing you want to build, you're stuck on one particular thing and you really need help right now! Thing is, that's unlikely to happen. Badgering individuals, or a whole resource, to reply right now, or complaining that it's been $TIME_PERIOD since you asked and nobody has replied... that's just going to make people less likely to reply.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#unnecessarily-tag-individuals","title":"Unnecessarily tag individuals","text":"

    This one often goes hand in hand with the \"lack patience\" thing: Be it asking on Discord, or in GitHub issues, discussions or even PRs, unnecessarily tagging individuals is a bit rude. Speaking for myself and only myself: I love helping folk with Textual. If I could help everyone all the time the moment they have a problem, I would. But it doesn't work like that. There's any number of reasons I might not be responding to a particular request, including but not limited to (here I'm talking personally because I don't want to speak for anyone else, but I'm sure I'm not alone here):

    • I have a job. Sure, my job is (in part) Textual, but there's more to it than that particular issue. I might be doing other stuff.
    • I have my own projects to work on too. I like coding for fun as well (or writing preaching old dude blog posts like this I guess, but you get the idea).
    • I actually have other interests outside of work hours so I might actually be out doing a 10k in the local glen, or battling headcrabs in VR, or something.
    • Housework. :-/

    You get the idea though. So while I'm off having a well-rounded life, it's not good to get unnecessarily intrusive alerts to something that either a) doesn't actually directly involve me or b) could wait.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#seek-personal-support","title":"Seek personal support","text":"

    Again, I'm going to speak totally for myself here, but I also feel the general case is polite for all: there's a lot of good support resources available already; sending DMs on Discord or Twitter or in the Fediverse, looking for direct personal support, isn't really the best way to get help. Using the public/collective resources is absolutely the best way to get that help. Why's it a bad idea to dive into DMs? Here's some reasons I think it's not a good idea:

    • It's a variation on \"unnecessarily tagging individuals\".
    • You're short-changing yourself when it comes to getting help. If you ask somewhere more public you're asking a much bigger audience, who collectively have more time, more knowledge and more experience than a single individual.
    • Following on from that, any answers can be (politely) fact-checked or enhanced by that audience, resulting in a better chance of getting the best help possible.
    • The next seeker-of-help gets to miss out on your question and the answer. If asked and answered in public, it's a record that can help someone else in the future.
    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#doubt-your-ability-or-skill-level","title":"Doubt your ability or skill level","text":"

    I suppose this should really be phrased as a do rather than a don't, as here I want to encourage something positive. A few times I've helped people out who have been very apologetic about their questions being \"noob\" questions, or about how they're fairly new to Python, or programming in general. Really, please, don't feel the need to apologise and don't be ashamed of where you're at.

    If you've asked something that's obviously answered in the documentation, that's not a problem; you'll likely get pointed at the docs and it's what happens next that's the key bit. If the attitude is \"oh, cool, that's exactly what I needed to be reading, thanks!\" that's a really positive thing. The only time it's a problem is when there's a real reluctance to use the available resources. We've all seen that person somewhere at some point, right? ;-)

    Not knowing things is totally cool.

    "},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#conclusion","title":"Conclusion","text":"

    So, that's my waffle over. As I said at the start: this is my own personal thoughts on how to get help with Textual, both as someone whose job it is to work on Textual and help people with Textual, and also as a FOSS advocate and supporter who can normally be found helping Textual users when he's not \"on the clock\" too.

    What I've written here isn't exhaustive. Neither is it novel. Plenty has been written on the general subject in the past, and I'm sure more will be written on the subject in the future. I do, however, feel that these are the most common things I notice. I'd say those dos and don'ts cover 90% of \"can I get some help?\" interactions; perhaps closer to 99%.

    Finally, and I think this is the most important thing to remember, the next time you are battling some issue while working with Textual: don't lose your head!

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/","title":"On dog food, the (original) Metaverse, and (not) being bored","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#introduction","title":"Introduction","text":"

    Quote

    Cutler, armed with a schedule, was urging the team to \"eat its own dog food\". Part macho stunt and part common sense, the \"dog food diet\" was the cornerstone of Cutler\u2019s philosophy.

    G. Pascal Zachary \u2014 Show-Stopper!

    I can't remember exactly when it was -- it was likely late in 1994 or some time in 1995 -- when I first came across the concept of, or rather the name for the concept of, \"eating your own dog food\". The idea and the name played a huge part in the book Show-Stopper! by G. Pascal Zachary. The idea wasn't new to me of course; I'd been writing code for over a decade by then and plenty of times I'd built things and then used those things to do things, but it was fascinating to a mostly-self-taught 20-something me to be reading this (excellent -- go read it if you care about the history of your craft) book and to see the idea written down and named.

    While Textualize isn't (thankfully -- really, I do recommend reading the book) anything like working on the team building Windows NT, the idea of taking a little time out from working on Textual, and instead work with Textual, makes a lot of sense. It's far too easy to get focused on adding things and improving things and tweaking things while losing sight of the fact that people will want to build with your product.

    So you can imagine how pleased I was when Will announced that he wanted all of us to spend a couple or so weeks building something with Textual. I had, of course, already written one small application with the library, and had plans for another (in part it's how I ended up working here), but I'd yet to really dive in and try and build something more involved.

    Giving it some thought: I wasn't entirely sure what I wanted to build though. I do want to use Textual to build a brand new terminal-based Norton Guide reader (not my first, not by a long way) but I felt that was possibly a bit too niche, and actually could take a bit too long anyway. Maybe not, it remains to be seen.

    Eventually I decided on this approach: try and do a quick prototype of some daft idea each day or each couple of days, do that for a week or so, and then finally try and settle down on something less trivial. This approach should work well in that it'll help introduce me to more of Textual, help try out a few different parts of the library, and also hopefully discover some real pain-points with working with it and highlight a list of issues we should address -- as seen from the perspective of a developer working with the library.

    So, here I am, at the end of week one. What I want to try and do is briefly (yes yes, I know, this introduction is the antithesis of brief) talk about what I built and perhaps try and highlight some lessons learnt, highlight some patterns I think are useful, and generally do an end-of-week version of a TIL. TWIL?

    Yeah. I guess this is a TWIL.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#gridinfo","title":"gridinfo","text":"

    I started the week by digging out a quick hack I'd done a couple of weeks earlier, with a view to cleaning it up. It started out as a fun attempt to do something with Rich Pixels while also making a terminal-based take on slstats.el. I'm actually pleased with the result and how quickly it came together.

    The point of the application itself is to show some general information about the current state of the Second Life grid (hello to any fellow residents of the original Metaverse!), and to also provide a simple region lookup screen that, using Rich Pixels, will display the object map (albeit in pretty low resolution -- but that's the fun of this!).

    So the opening screen looks like this:

    and a lookup of a region looks like this:

    Here's a wee video of the whole thing in action:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight","title":"Worth a highlight","text":"

    Here's a couple of things from the code that I think are worth a highlight, as things to consider when building Textual apps:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-use-the-default-screen","title":"Don't use the default screen","text":"

    Use of the default Screen that's provided by the App is handy enough, but I feel any non-trivial application should really put as much code as possible in screens that relate to key \"work\". Here's the entirety of my application code:

    class GridInfo( App[ None ] ):\n    \"\"\"TUI app for showing information about the Second Life grid.\"\"\"\n\n    CSS_PATH = \"gridinfo.css\"\n    \"\"\"The name of the CSS file for the app.\"\"\"\n\n    TITLE = \"Grid Information\"\n    \"\"\"str: The title of the application.\"\"\"\n\n    SCREENS = {\n        \"main\": Main,\n        \"region\": RegionInfo\n    }\n    \"\"\"The collection of application screens.\"\"\"\n\n    def on_mount( self ) -> None:\n        \"\"\"Set up the application on startup.\"\"\"\n        self.push_screen( \"main\" )\n

    You'll notice there's no work done in the app, other than to declare the screens, and to set the main screen running when the app is mounted.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-work-hard-on_mount","title":"Don't work hard on_mount","text":"

    My initial version of the application had it loading up the data from the Second Life and GridSurvey APIs in Main.on_mount. This obviously wasn't a great idea as it made the startup appear slow. That's when I realised just how handy call_after_refresh is. This meant I could show some placeholder information and then fire off the requests (3 of them: one to get the main grid information, one to get the grid concurrency data, and one to get the grid size data), keeping the application looking active and updating the display when the replies came in.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points","title":"Pain points","text":"

    While building this app I think there was only really the one pain-point, and I suspect it's mostly more on me than on Textual itself: getting a good layout and playing whack-a-mole with CSS. I suspect this is going to be down to getting more and more familiar with CSS and the terminal (which is different from laying things out for the web), while also practising with various layout schemes -- which is where the revamped Placeholder class is going to be really useful.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#unbored","title":"unbored","text":"

    The next application was initially going to be a very quick hack, but actually turned into a less-trivial build than I'd initially envisaged; not in a negative way though. The more I played with it the more I explored and I feel that this ended up being my first really good exploration of some useful (personal -- your kilometerage may vary) patterns and approaches when working with Textual.

    The application itself is a terminal client for the Bored-API. I had initially intended to roll my own code for working with the API, but I noticed that someone had done a nice library for it and it seemed silly to not build on that. Not needing to faff with that, I could concentrate on the application itself.

    At first I was just going to let the user click away at a button that showed a random activity, but this quickly morphed into a \"why don't I make this into a sort of TODO list builder app, where you can add things to do when you are bored, and delete things you don't care for or have done\" approach.

    Here's a view of the main screen:

    and here's a view of the filter pop-over:

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight_1","title":"Worth a highlight","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-put-all-your-bindings-in-one-place","title":"Don't put all your BINDINGS in one place","text":"

    This came about from me overloading the use of the escape key. I wanted it to work more or less like this:

    • If you're inside an activity, move focus up to the activity type selection buttons.
    • If the filter pop-over is visible, close that.
    • Otherwise exit the application.

    It was easy enough to do, and I had an action in the Main screen that escape was bound to (again, in the Main screen) that did all this logic with some if/elif work but it didn't feel elegant. Moreover, it meant that the Footer always displayed the same description for the key.

    That's when I realised that it made way more sense to have a Binding for escape in every widget that was the actual context for escape's use. So I went from one top-level binding to...

    ...\n\nclass Activity( Widget ):\n    \"\"\"A widget that holds and displays a suggested activity.\"\"\"\n\n    BINDINGS = [\n        ...\n        Binding( \"escape\", \"deselect\", \"Switch to Types\" )\n    ]\n\n...\n\nclass Filters( Vertical ):\n    \"\"\"Filtering sidebar.\"\"\"\n\n    BINDINGS = [\n        Binding( \"escape\", \"close\", \"Close Filters\" )\n    ]\n\n...\n\nclass Main( Screen ):\n    \"\"\"The main application screen.\"\"\"\n\n    BINDINGS = [\n        Binding( \"escape\", \"quit\", \"Close\" )\n    ]\n    \"\"\"The bindings for the main screen.\"\"\"\n

    This was so much cleaner and I got better Footer descriptions too. I'm going to be leaning hard on this approach from now on.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#messages-are-awesome","title":"Messages are awesome","text":"

    Until I wrote this application I hadn't really had a need to define or use my own Messages. During work on this I realised how handy they really are. In the code I have an Activity widget which takes care of the job of moving itself amongst its siblings if the user asks to move an activity up or down. When this happens I also want the Main screen to save the activities to the filesystem as things have changed.

    Thing is: I don't want the screen to know what an Activity is capable of and I don't want an Activity to know what the screen is capable of; especially the latter as I really don't want a child of a screen to know what the screen can do (in this case \"save stuff\").

    This is where messages come in. Using a message I could just set things up so that the Activity could shout out \"HEY I JUST DID A THING THAT CHANGES ME\" and not care who is listening and not care what they do with that information.

    So, thanks to this bit of code in my Activity widget...

        class Moved( Message ):\n        \"\"\"A message to indicate that an activity has moved.\"\"\"\n\n    def action_move_up( self ) -> None:\n        \"\"\"Move this activity up one place in the list.\"\"\"\n        if self.parent is not None and not self.is_first:\n            parent = cast( Widget, self.parent )\n            parent.move_child(\n                self, before=parent.children.index( self ) - 1\n            )\n            self.emit_no_wait( self.Moved( self ) )\n            self.scroll_visible( top=True )\n

    ...the Main screen can do this:

        def on_activity_moved( self, _: Activity.Moved ) -> None:\n        \"\"\"React to an activity being moved.\"\"\"\n        self.save_activity_list()\n

    Warning

    The code above used emit_no_wait. Since this blog post was first published that method has been removed from Textual. You should use post_message_no_wait or post_message instead now.

    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points_1","title":"Pain points","text":"

    On top of the issues of getting to know terminal-based-CSS that I mentioned earlier:

    • Textual currently lacks any sort of selection list or radio-set widget. This meant that I couldn't quite do the activity type picking how I would have wanted. Of course I could have rolled my own widgets for this, but I think I'd sooner wait until such things are in Textual itself.
    • Similar to that, I could have used some validating Input widgets. They too are on the roadmap but I managed to cobble together fairly good working versions for my purposes. In doing so though I did further highlight that the reactive attribute facility needs a wee bit more attention as I ran into some (already-known) bugs. Thankfully in my case it was a very easy workaround.
    • Scrolling in general seems a wee bit off when it comes to widgets that are more than one line tall. While there's nothing really obvious I can point my finger at, I'm finding that scrolling containers sometimes get confused about what should be in view. This becomes very obvious when forcing things to scroll from code. I feel this deserves a dedicated test application to explore this more.
    "},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#conclusion","title":"Conclusion","text":"

    The first week of \"dogfooding\" has been fun and I'm more convinced than ever that it's an excellent exercise for Textualize to engage in. I didn't quite manage my plan of \"one silly trivial prototype per day\", which means I've ended up with two (well technically one and a half I guess given that gridinfo already existed as a prototype) applications rather than four. I'm okay with that. I got a lot of utility out of this.

    Now to look at the list of ideas I have going and think about what I'll kick next week off with...

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/","title":"What I learned from my first non-trivial PR","text":"PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

    It's 8:59 am and, by my Portuguese standards, it is freezing cold outside: 5 or 6 degrees Celsius. It is my second day at Textualize and I just got into the office. I undress my many layers of clothing to protect me from the Scottish cold and I sit down in my improvised corner of the Textualize office. As I sit down, I turn myself in my chair to face my boss and colleagues to ask \u201cSo, what should I do today?\u201d. I was not expecting Will's answer, but the challenge excited me:

    \u201cI thought I'll just throw you in the deep end and have you write some code.\u201d

    What happened next was that I spent two days working on PR #1229 to add a new widget to the Textual code base. At the time of writing, the pull request has not been merged yet. Well, to be honest with you, it hasn't even been reviewed by anyone... But that won't stop me from blogging about some of the things I learned while creating this PR.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#the-placeholder-widget","title":"The placeholder widget","text":"

    This PR adds a widget called Placeholder to Textual. As per the documentation, this widget \u201cis meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.\u201d

    The point of the placeholder widget is that you can focus on building the layout of your app without having to have all of your (custom) widgets ready. The placeholder widget also displays a couple of useful pieces of information to help you work out the layout of your app, namely the ID of the widget itself (or a custom label, if you provide one) and the width and height of the widget.

    As an example of usage of the placeholder widget, you can refer to the screenshot at the top of this blog post, which I included below so you don't have to scroll up:

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

    The top left and top right widgets have custom labels. Immediately under the top right placeholder, you can see some placeholders identified as #p3, #p4, and #p5. Those are the IDs of the respective placeholders. Then, rows 2 and 3 contain some placeholders that show their respective size and some placeholders that just contain some text.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#bootstrapping-the-code-for-the-widget","title":"Bootstrapping the code for the widget","text":"

    So, how does a code monkey start working on a non-trivial PR within 24 hours of joining a company? The answer is simple: just copy and paste code! But instead of copying and pasting from Stack Overflow, I decided to copy and paste from the internal code base.

    My task was to create a new widget, so I thought it would be a good idea to take a look at the implementation of other Textual widgets. For some reason I cannot seem to recall, I decided to take a look at the implementation of the button widget that you can find in _button.py. By looking at how the button widget is implemented, I could immediately learn a few useful things about what I needed to do and some other things about how Textual works.

    For example, a widget can have a class attribute called DEFAULT_CSS that specifies the default CSS for that widget. I learned this just from staring at the code for the button widget.

    Studying the code base will also reveal the standards that are in place. For example, I learned that for a widget with variants (like the button with its \u201csuccess\u201d and \u201cerror\u201d variants), the widget gets a CSS class with the name of the variant prefixed by a dash. You can learn this by looking at the method Button.watch_variant:

    class Button(Static, can_focus=True):\n    # ...\n\n    def watch_variant(self, old_variant: str, variant: str):\n        self.remove_class(f\"-{old_variant}\")\n        self.add_class(f\"-{variant}\")\n

    In short, looking at code and files that are related to the things you need to do is a great way to get information about things you didn't even know you needed.

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#handling-the-placeholder-variant","title":"Handling the placeholder variant","text":"

    A button widget can have a different variant, which is mostly used by Textual to determine the CSS that should apply to the given button. For the placeholder widget, we want the variant to determine what information the placeholder shows. The original GitHub issue mentions 5 variants for the placeholder:

    • a variant that just shows a label or the placeholder ID;
    • a variant that shows the size and location of the placeholder;
    • a variant that shows the state of the placeholder (does it have focus? is the mouse over it?);
    • a variant that shows the CSS that is applied to the placeholder itself; and
    • a variant that shows some text inside the placeholder.

    The variant can be assigned when the placeholder is first instantiated, for example, Placeholder(\"css\") would create a placeholder that shows its own CSS. However, we also want to have an on_click handler that cycles through all the possible variants. I was getting ready to reinvent the wheel when I remembered that the standard module itertools has a lovely tool that does exactly what I needed! Thus, all I needed to do was create a new cycle through the variants each time a placeholder is created and then grab the next variant whenever the placeholder is clicked:

    class Placeholder(Static):\n    def __init__(\n        self,\n        variant: PlaceholderVariant = \"default\",\n        *,\n        label: str | None = None,\n        name: str | None = None,\n        id: str | None = None,\n        classes: str | None = None,\n    ) -> None:\n        # ...\n\n        self.variant = self.validate_variant(variant)\n        # Set a cycle through the variants with the correct starting point.\n        self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\n        while next(self._variants_cycle) != self.variant:\n            pass\n\n    def on_click(self) -> None:\n        \"\"\"Click handler to cycle through the placeholder variants.\"\"\"\n        self.cycle_variant()\n\n    def cycle_variant(self) -> None:\n        \"\"\"Get the next variant in the cycle.\"\"\"\n        self.variant = next(self._variants_cycle)\n

    I am just happy that I had the insight to add this little while loop when a placeholder is instantiated:

    from itertools import cycle\n# ...\nclass Placeholder(Static):\n    # ...\n    def __init__(...):\n        # ...\n        self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\n        while next(self._variants_cycle) != self.variant:\n            pass\n

    Can you see what would be wrong if this loop wasn't there?

    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#updating-the-render-of-the-placeholder-on-variant-change","title":"Updating the render of the placeholder on variant change","text":"

    If the variant of the placeholder is supposed to determine what information the placeholder shows, then that information must be updated every time the variant of the placeholder changes. Thankfully, Textual has reactive attributes and watcher methods, so all I needed to do was... Defer the problem to another method:

    class Placeholder(Static):\n    # ...\n    variant = reactive(\"default\")\n    # ...\n    def watch_variant(\n        self, old_variant: PlaceholderVariant, variant: PlaceholderVariant\n    ) -> None:\n        self.validate_variant(variant)\n        self.remove_class(f\"-{old_variant}\")\n        self.add_class(f\"-{variant}\")\n        self.call_variant_update()  # <-- let this method do the heavy lifting!\n

    Doing this properly required some thinking. Not that the current proposed solution is the best possible, but I did think of worse alternatives while I was thinking how to tackle this. I wasn't entirely sure how I would manage the variant-dependant rendering because I am not a fan of huge conditional statements that look like switch statements:

    if variant == \"default\":\n    # render the default placeholder\nelif variant == \"size\":\n    # render the placeholder with its size\nelif variant == \"state\":\n    # render the state of the placeholder\nelif variant == \"css\":\n    # render the placeholder with its CSS rules\nelif variant == \"text\":\n    # render the placeholder with some text inside\n

    However, I am a fan of using the built-in getattr and I thought of creating a rendering method for each different variant. Then, all I needed to do was make sure the variant is part of the name of the method so that I can programmatically determine the name of the method that I need to call. This means that the method Placeholder.call_variant_update is just this:

    class Placeholder(Static):\n    # ...\n    def call_variant_update(self) -> None:\n        \"\"\"Calls the appropriate method to update the render of the placeholder.\"\"\"\n        update_variant_method = getattr(self, f\"_update_{self.variant}_variant\")\n        update_variant_method()\n

    If self.variant is, say, \"size\", then update_variant_method refers to _update_size_variant:

    class Placeholder(Static):\n    # ...\n    def _update_size_variant(self) -> None:\n        \"\"\"Update the placeholder with the size of the placeholder.\"\"\"\n        width, height = self.size\n        self._placeholder_label.update(f\"[b]{width} x {height}[/b]\")\n

    This variant \"size\" also interacts with resizing events, so we have to watch out for those:

    class Placeholder(Static):\n    # ...\n    def on_resize(self, event: events.Resize) -> None:\n        \"\"\"Update the placeholder \"size\" variant with the new placeholder size.\"\"\"\n        if self.variant == \"size\":\n            self._update_size_variant()\n
    "},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#deleting-code-is-a-hurtful-blessing","title":"Deleting code is a (hurtful) blessing","text":"

    To conclude this blog post, let me muse about the fact that the original issue mentioned five placeholder variants and that my PR only includes two and a half.

    After careful consideration and after coming up with the getattr mechanism to update the display of the placeholder according to the active variant, I started showing the \u201cfinal\u201d product to Will and my other colleagues. Eventually, we ended up getting rid of the variant for CSS and the variant that shows the placeholder state. This means that I had to delete part of my code even before it saw the light of day.

    On the one hand, deleting those chunks of code made me a bit sad. After all, I had spent quite some time thinking about how to best implement that functionality! But then, it was time to write documentation and tests, and I verified that the best code is the code that you don't even write! The code you don't write is guaranteed to have zero bugs and it also does not need any documentation whatsoever!

    So, it was a shame that some lines of code I poured my heart and keyboard into did not get merged into the Textual code base. On the other hand, I am quite grateful that I won't have to fix the bugs that will certainly reveal themselves in a couple of weeks or months from now. Heck, the code hasn't been merged yet and just by writing this blog post I noticed a couple of tweaks that were missing!

    "},{"location":"blog/2023/07/29/pull-requests-are-cake-or-puppies/","title":"Pull Requests are cake or puppies","text":"

    Broadly speaking, there are two types of contributions you can make to an Open Source project.

    The first type is typically a bug fix, but could also be a documentation update, linting fix, or other change which doesn't impact core functionality. Such a contribution is like cake. It's a simple, delicious, gift to the project.

    The second type of contribution often comes in the form of a new feature. This contribution likely represents a greater investment of time and effort than a bug fix. It is still a gift to the project, but this contribution is not cake.

    A feature PR has far more in common with a puppy. The maintainer(s) may really like the feature but hesitate to merge all the same. They may even reject the contribution entirely. This is because a feature PR requires an ongoing burden to maintain. In the same way that a puppy needs food and walkies, a new feature will require updates and fixes long after the original contribution. Even if it is an amazing feature, the maintainer may not want to commit to that ongoing work.

    The chances of a feature being merged can depend on the maturity of the project. At the beginning of a project, a maintainer may be delighted with a new feature contribution. After all, having others join you to build something is the joy of Open Source. And yet when a project gets more mature there may be a growing resistance to adding new features, and a greater risk that a feature PR is rejected or sits unappreciated in the PR queue.

    So how should a contributor avoid this? If there is any doubt, it's best to propose the feature to the maintainers before undertaking the work. In all likelihood they will be happy for your contribution, just be prepared for them to say \"thanks but no thanks\". Don't take it as a rejection of your gift: it's just that the maintainer can't commit to taking on a puppy.

    There are other ways to contribute code to a project that don't require the code to be merged in to the core. You could publish your change as a third party library. Take it from me: maintainers love it when their project spawns an ecosystem. You could also blog about how you solved your problem without an update to the core project. Having a resource that can be googled for, or a maintainer can direct people to, can be a huge help.

    What prompted me to think about this is that my two main projects, Rich and Textual, are at quite different stages in their lifetime. Rich is relatively mature, and I'm unlikely to accept a puppy. If you can achieve what you need without adding to the core library, I am probably going to decline a new feature. Textual is younger and still accepting puppies \u2014 in addition to stick insects, gerbils, capybaras and giraffes.

    Tip

    If you are maintainer, and you do have to close a feature PR, feel free to link to this post.

    Join us on the Discord Server if you want to discuss puppies and other creatures.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/","title":"Textual 0.11.0 adds a beautiful Markdown widget","text":"

    We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have we been up to?

    The headline feature of this release is the enhanced Markdown support. Here's a screenshot of an example:

    MarkdownApp \u258bHeader\u00a0level\u00a06\u00a0content. \u25bc\u00a0\u2160\u00a0Textual\u00a0Markdown\u00a0Browser\u00a0-\u00a0Demo\u258b \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Headers\u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2162\u00a0This\u00a0is\u00a0H3\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2163\u00a0This\u00a0is\u00a0H4\u258b\u258eTypography\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2164\u00a0This\u00a0is\u00a0H5\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u2165\u00a0This\u00a0is\u00a0H6\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Typography\u258bThe\u00a0usual\u00a0Markdown\u00a0typography\u00a0is\u00a0supported.\u00a0The\u00a0exact\u00a0output\u00a0depends\u00a0on\u00a0 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Emphasis\u258byour\u00a0terminal,\u00a0although\u00a0most\u00a0are\u00a0fairly\u00a0consistent.\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strong\u258b \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strikethrough\u258bEmphasis \u2502\u00a0\u00a0\u00a0\u2517\u2501\u2501\u00a0\u2162\u00a0Inline\u00a0code\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u2161\u00a0Fences\u258bEmphasis\u00a0is\u00a0rendered\u00a0with\u00a0*asterisks*,\u00a0and\u00a0looks\u00a0like\u00a0this; \u251c\u2500\u2500\u00a0\u2161\u00a0Quote\u258b \u2514\u2500\u2500\u00a0\u2161\u00a0Tables\u258bStrong \u258b\u2594\u2594\u2594\u2594\u2594\u2594 \u258bUse\u00a0two\u00a0asterisks\u00a0to\u00a0indicate\u00a0strong\u00a0which\u00a0renders\u00a0in\u00a0bold,\u00a0e.g.\u00a0 \u258b**strong**\u00a0render\u00a0strong. \u258b \u258bStrikethrough \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bTwo\u00a0tildes\u00a0indicates\u00a0strikethrough,\u00a0e.g.\u00a0~~cross\u00a0out~~\u00a0render\u00a0cross\u00a0out. \u258b\u2582\u2582 \u258bInline\u00a0code \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bInline\u00a0code\u00a0is\u00a0indicated\u00a0by\u00a0backticks.\u00a0e.g.\u00a0import\u00a0this. \u258b \u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258b\u258e\u258b \u258b\u258eFences\u258b \u258b\u258e\u258b \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bFenced\u00a0code\u00a0blocks\u00a0are\u00a0introduced\u00a0with\u00a0three\u00a0back-ticks\u00a0and\u00a0the\u00a0optional\u00a0 \u258bparser.\u00a0Here\u00a0we\u00a0are\u00a0rendering\u00a0the\u00a0code\u00a0in\u00a0a\u00a0sub-widget\u00a0with\u00a0syntax\u00a0 \u258bhighlighting\u00a0and\u00a0indent\u00a0guides. \u258b \u258bIn\u00a0the\u00a0future\u00a0I\u00a0think\u00a0we\u00a0could\u00a0add\u00a0controls\u00a0to\u00a0export\u00a0the\u00a0code,\u00a0copy\u00a0to\u00a0 \u258bthe\u00a0clipboard.\u00a0Heck,\u00a0even\u00a0run\u00a0it\u00a0and\u00a0show\u00a0the\u00a0output? \u258b \u258b \u258b@lru_cache(maxsize=1024) \u258bdefsplit(self,cut_x:int,cut_y:int)->tuple[Region,Region,Regi \u258b\u2502\u00a0\u00a0\u00a0\"\"\"Split\u00a0a\u00a0region\u00a0in\u00a0to\u00a04\u00a0from\u00a0given\u00a0x\u00a0and\u00a0y\u00a0offsets\u00a0(cuts). \u00a0T\u00a0\u00a0TOC\u00a0\u00a0B\u00a0\u00a0Back\u00a0\u00a0F\u00a0\u00a0Forward\u00a0

    Tip

    You can generate these SVG screenshots for your app with textual run my_app.py --screenshot 5 which will export a screenshot after 5 seconds.

    There are actually 2 new widgets: Markdown for a simple Markdown document, and MarkdownViewer which adds browser-like navigation and a table of contents.

    Textual has had support for Markdown since day one by embedding a Rich Markdown object -- which still gives decent results! This new widget adds dynamic controls such as scrollable code fences and tables, in addition to working links.

    In future releases we plan on adding more Markdown extensions, and the ability to easily embed custom widgets within the document. I'm sure there are plenty of interesting applications that could be powered by dynamically generated Markdown documents.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#datatable-improvements","title":"DataTable improvements","text":"

    There has been a lot of work on the DataTable API. We've added the ability to sort the data, which required that we introduce the concept of row and column keys. You can now reference rows / columns / cells by their coordinate or by row / column key.

    Additionally there are new update_cell and update_cell_at methods to update cells after the data has been populated. Future releases will have more methods to manipulate table data, which will make it a very general purpose (and powerful) widget.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#tree-control","title":"Tree control","text":"

    The Tree widget has grown a few methods to programmatically expand, collapse and toggle tree nodes.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#breaking-changes","title":"Breaking changes","text":"

    There are a few breaking changes in this release. These are mostly naming and import related, which should be easy to fix if you are affected. Here's a few notable examples:

    • Checkbox has been renamed to Switch. This is because we plan to introduce complimentary Checkbox and RadioButton widgets in a future release, but we loved the look of Switches too much to drop them.
    • We've dropped the emit and emit_no_wait methods. These methods posted message to the parent widget, but we found that made it problematic to subclass widgets. In almost all situations you want to replace these with self.post_message (or self.post_message_no_wait).

    Be sure to check the CHANGELOG for the full details on potential breaking changes.

    "},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#join-us","title":"Join us!","text":"

    We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/","title":"Textual 0.12.0 adds syntactical sugar and batch updates","text":"

    It's been just 9 days since the previous release, but we have a few interesting enhancements to the Textual API to talk about.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#better-compose","title":"Better compose","text":"

    We've added a little syntactical sugar to Textual's compose methods, which aids both readability and editability (that might not be a word).

    First, let's look at the old way of building compose methods. This snippet is taken from the textual colors command.

    for color_name in ColorSystem.COLOR_NAMES:\n\n    items: list[Widget] = [ColorLabel(f'\"{color_name}\"')]\n    for level in LEVELS:\n        color = f\"{color_name}-{level}\" if level else color_name\n        item = ColorItem(\n            ColorBar(f\"${color}\", classes=\"text label\"),\n            ColorBar(\"$text-muted\", classes=\"muted\"),\n            ColorBar(\"$text-disabled\", classes=\"disabled\"),\n            classes=color,\n        )\n        items.append(item)\n\n    yield ColorGroup(*items, id=f\"group-{color_name}\")\n

    This code composes the following color swatches:

    ColorsApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 primary \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b\u2581\u2581 secondary\u258e\"primary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b background\u258e$primary-darken-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b primary-background\u258e$primary-darken-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b secondary-background\u258e$primary-darken-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b surface\u258e$primary$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b panel\u258e$primary-lighten-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b boost\u258e$primary-lighten-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b warning\u258e$primary-lighten-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b error\u258e\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 success \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b accent\u258e\"secondary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u258e\u258b \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

    Tip

    You can see this by running textual colors from the command line.

    The old way was not all that bad, but it did make it hard to see the structure of your app at-a-glance, and editing compose methods always felt a little laborious.

    Here's the new syntax, which uses context managers to add children to containers:

    for color_name in ColorSystem.COLOR_NAMES:\n    with ColorGroup(id=f\"group-{color_name}\"):\n        yield Label(f'\"{color_name}\"')\n        for level in LEVELS:\n            color = f\"{color_name}-{level}\" if level else color_name\n            with ColorItem(classes=color):\n                yield ColorBar(f\"${color}\", classes=\"text label\")\n                yield ColorBar(\"$text-muted\", classes=\"muted\")\n                yield ColorBar(\"$text-disabled\", classes=\"disabled\")\n

    The context manager approach generally results in fewer lines of code, and presents attributes on the same line as containers themselves. Additionally, adding widgets to a container can be as simple is indenting them.

    You can still construct widgets and containers with positional arguments, but this new syntax is preferred. It's not documented yet, but you can start using it now. We will be updating our examples in the next few weeks.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#batch-updates","title":"Batch updates","text":"

    Textual is smart about performing updates to the screen. When you make a change that might repaint the screen, those changes don't happen immediately. Textual makes a note of them, and repaints the screen a short time later (around a 1/60th of a second). Multiple updates are combined so that Textual does less work overall, and there is none of the flicker you might get with multiple repaints.

    Although this works very well, it is possible to introduce a little flicker if you make changes across multiple widgets. And especially if you add or remove many widgets at once. To combat this we have added a batch_update context manager which tells Textual to disable screen updates until the end of the with block.

    The new Markdown widget uses this context manager when it updates its content. Here's the code:

    with self.app.batch_update():\n    await self.query(\"MarkdownBlock\").remove()\n    await self.mount_all(output)\n

    Without the batch update there are a few frames where the old markdown blocks are removed and the new blocks are added (which would be perceived as a brief flicker). With the update, the update appears instant.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#disabled-widgets","title":"Disabled widgets","text":"

    A few widgets (such as Button) had a disabled attribute which would fade the widget a little and make it unselectable. We've extended this to all widgets. Although it is particularly applicable to input controls, anything may be disabled. Disabling a container makes its children disabled, so you could use this for disabling a form, for example.

    Tip

    Disabled widgets may be styled with the :disabled CSS pseudo-selector.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#preventing-messages","title":"Preventing messages","text":"

    Also in this release is another context manager, which will disable specified Message types. This doesn't come up as a requirement very often, but it can be very useful when it does. This one is documented, see Preventing events for details.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#full-changelog","title":"Full changelog","text":"

    As always see the release page for additional changes and bug fixes.

    "},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#join-us","title":"Join us!","text":"

    We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

    "},{"location":"blog/2023/03/09/textual-0140-shakes-up-posting-messages/","title":"Textual 0.14.0 shakes up posting messages","text":"

    Textual version 0.14.0 has landed just a week after 0.13.0.

    Note

    We like fast releases for Textual. Fast releases means quicker feedback, which means better code.

    What's new?

    We did a little shake-up of posting messages which will simplify building widgets. But this does mean a few breaking changes.

    There are two methods in Textual to post messages: post_message and post_message_no_wait. The former was asynchronous (you needed to await it), and the latter was a regular method call. These two methods have been replaced with a single post_message method.

    To upgrade your project to Textual 0.14.0, you will need to do the following:

    • Remove await keywords from any calls to post_message.
    • Replace any calls to post_message_no_wait with post_message.

    Additionally, we've simplified constructing messages classes. Previously all messages required a sender argument, which had to be manually set. This was a clear violation of our \"no boilerplate\" policy, and has been dropped. There is still a sender property on messages / events, but it is set automatically.

    So prior to 0.14.0 you might have posted messages like the following:

    await self.post_message(self.Changed(self, item=self.item))\n

    You can now replace it with this simpler function call:

    self.post_message(self.Change(item=self.item))\n

    This also means that you will need to drop the sender from any custom messages you have created.

    If this was code pre-0.14.0:

    class MyWidget(Widget):\n\n    class Changed(Message):\n        \"\"\"My widget change event.\"\"\"\n        def __init__(self, sender:MessageTarget, item_index:int) -> None:\n            self.item_index = item_index\n            super().__init__(sender)\n

    You would need to make the following change (dropping sender).

    class MyWidget(Widget):\n\n    class Changed(Message):\n        \"\"\"My widget change event.\"\"\"\n        def __init__(self, item_index:int) -> None:\n            self.item_index = item_index\n            super().__init__()\n

    If you have any problems upgrading, join our Discord server, we would be happy to help.

    See the release notes for the full details on this update.

    "},{"location":"blog/2023/03/13/textual-0150-adds-a-tabs-widget/","title":"Textual 0.15.0 adds a tabs widget","text":"

    We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical release cadence of 1 to 2 weeks.

    What's new in this release?

    The highlight of this release is a new Tabs widget to display tabs which can be navigated much like tabs in a browser. Here's a screenshot:

    TabsApp Paul\u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0Halleck \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    In a future release, this will be combined with the ContentSwitcher widget to create a traditional tabbed dialog. Although Tabs is still useful as a standalone widgets.

    Tip

    I like to tweet progress with widgets on Twitter. See the #textualtabs hashtag which documents progress on this widget.

    Also in this release is a new LoadingIndicator widget to display a simple animation while waiting for data. Here's a screenshot:

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    As always, see the release notes for the full details on this update.

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

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/","title":"Textual 0.16.0 adds TabbedContent and border titles","text":"

    Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.

    There are two highlights in this release. In no particular order, the first is TabbedContent which uses a row of tabs to navigate content. You will have likely encountered this UI in the desktop and web. I think in Windows they are known as \"Tabbed Dialogs\".

    This widget combines existing Tabs and ContentSwitcher widgets and adds an expressive interface for composing. Here's a trivial example to use content tabs to navigate a set of three markdown documents:

    def compose(self) -> ComposeResult:\n    with TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\n        yield Markdown(LETO)\n        yield Markdown(JESSICA)\n        yield Markdown(PAUL)\n

    Here's an example of the UI you can create with this widget (note the nesting)!

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#border-titles","title":"Border titles","text":"

    The second highlight is a frequently requested feature (FRF?). Widgets now have the two new string properties, border_title and border_subtitle, which will be displayed within the widget's border.

    You can set the alignment of these titles via border-title-align and border-subtitle-align. Titles may contain Console Markup, so you can add additional color and style to the labels.

    Here's an example of a widget with a title:

    BorderApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 ascii \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 none \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550double\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 hidden\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0must\u00a0not\u00a0fear.\u2551 blank\u2551Fear\u00a0is\u00a0the\u00a0mind-killer.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2551 round\u2551I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551path.\u2551 solid\u2586\u2586\u2551Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551remain.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551\u2551 double\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 dashed \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 heavy \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BTW the above is a command you can run to see the various border styles you can apply to widgets.

    textual borders\n
    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#container-changes","title":"Container changes","text":"

    Breaking change

    If you have an app that uses any container classes, you should read this section.

    We've made a change to containers in this release. Previously all containers had auto scrollbars, which means that any container would scroll if its children didn't fit. With nested layouts, it could be tricky to understand exactly which containers were scrolling. In 0.16.0 we split containers in to scrolling and non-scrolling versions. So Horizontal will now not scroll by default, but HorizontalScroll will have automatic scrollbars.

    "},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#what-else","title":"What else?","text":"

    As always, see the release notes for the full details on this update.

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

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/","title":"Textual 0.17.0 adds translucent screens and Option List","text":"

    This is a surprisingly large release, given it has been just 7 days since the last version (and we were down a developer for most of that time).

    What's new in this release?

    There are two new notable features I want to cover. The first is a compositor effect.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#translucent-screens","title":"Translucent screens","text":"

    Textual has a concept of \"screens\" which you can think of as independent UI modes, each with their own user interface and logic. The App class keeps a stack of these screens so you can switch to a new screen and later return to the previous screen.

    Screens

    See the guide to learn more about the screens API.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

    Screens can be used to build modal dialogs by pushing a screen with controls / buttons, and popping the screen when the user has finished with it. The problem with this approach is that there was nothing to indicate to the user that the original screen was still there, and could be returned to.

    In this release we have added alpha support to the Screen's background color which allows the screen underneath to show through, typically blended with a little color. Applying this to a screen makes it clear than the user can return to the previous screen when they have finished interacting with the modal.

    Here's how you can enable this effect with CSS:

    DialogScreen {\n    align: center middle;\n    background: $primary 30%;\n}\n

    Setting the background to $primary will make the background blue (with the default theme). The addition of 30% sets the alpha so that it will be blended with the background. Here's the kind of effect this creates:

    DialogApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588Good\u00a0for\u00a0natural\u00a0breaks\u00a0in\u00a0the\u00a0content,\u00a0that\u00a0don't\u00a0require\u00a0another\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588header.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258e\u258b\u2588\u258b\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u258eLists\u258b\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u258e\u258b\u2582\u2582\u2588\u258b\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2582\u2582\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u00a01.\u00a0Lists\u00a0can\u00a0be\u00a0ordered\u2588down\u00a0widgets.\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u00a02.\u00a0Lists\u00a0can\u00a0be\u00a0unordered\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u25cf\u00a0I\u00a0must\u00a0not\u00a0fear.\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25aa\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u2584\u2584\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u2023\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2588\u258b\u2588\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2022\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258b\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2b51\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u25aa\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588see\u00a0its\u00a0path.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25cf\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2588\u2588\u2582\u2582\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588remain.\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588Longer\u00a0list\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u00a0\u00a01.\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides,\u00a0head\u00a0of\u00a0House\u00a0Atreides\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u00a0headings.\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0O\u2588This\u00a0is\u00a0H5\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0oblit\u2588Header\u00a0level\u00a05\u00a0content.\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H6\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588This\u00a0is\u00a0H4\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588Header\u00a0level\u00a04\u00a0content.\u00a0Drilling\u00a0down\u00a0in\u00a0to\u00a0finer\u00a0headings.\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H5\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588Header\u00a0level\u00a05\u00a0content.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588This\u00a0is\u00a0H6\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.

    There are 4 screens in the above screenshot, one for the base screen and one for each of the three dialogs. Note how each screen modifies the color of the screen below, but leaves everything visible.

    See the docs on screen opacity if you want to add this to your apps.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#option-list","title":"Option list","text":"

    Textual has had a ListView widget for a while, which is an excellent way of navigating a list of items (actually other widgets). In this release we've added an OptionList which is similar in appearance, but uses the line api under the hood. The Line API makes it more efficient when you approach thousands of items.

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    The Options List accepts Rich renderable, which means that anything Rich can render may be displayed in a list. Here's an Option List of tables:

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hephaestus\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25026.7\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hades\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Caprica\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    We plan to build on the OptionList widget to implement drop-downs, menus, check lists, etc. But it is still very useful as it is, and you can add it to apps now.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#what-else","title":"What else?","text":"

    There are a number of fixes regarding refreshing in this release. If you had issues with parts of the screen not updating, the new version should resolve it.

    There's also a new logging handler, and a \"thick\" border type.

    See release notes for the full details.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#next-week","title":"Next week","text":"

    Next week we plan to take a break from building Textual to building apps with Textual. We do this now and again to give us an opportunity to step back and understand things from the perspective of a developer using Textual. We will hopefully have something interesting to show from the exercise, and new Open Source apps to share.

    "},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/","title":"Textual 0.18.0 adds API for managing concurrent workers","text":"

    Less than a week since the last release, and we have a new API to show you.

    This release adds a new Worker API designed to manage concurrency, both asyncio tasks and threads.

    An API to manage concurrency may seem like a strange addition to a library for building user interfaces, but on reflection it makes a lot of sense. People are building Textual apps to interface with REST APIs, websockets, and processes; and they are running into predictable issues. These aren't specifically Textual problems, but rather general problems related to async tasks and threads. It's not enough for us to point users at the asyncio docs, we needed a better answer.

    The new run_worker method provides an easy way of launching \"Workers\" (a wrapper over async tasks and threads) which also manages their lifetime.

    One of the challenges I've found with tasks and threads is ensuring that they are shut down in an orderly manner. Interestingly enough, Textual already implemented an orderly shutdown procedure to close the tasks that power widgets: children are shut down before parents, all the way up to the App (the root node). The new API piggybacks on to that existing mechanism to ensure that worker tasks are also shut down in the same order.

    Tip

    You won't need to worry about this gnarly issue with the new Worker API.

    I'm particularly pleased with the new @work decorator which can turn a coroutine OR a regular function into a Textual Worker object, by scheduling it as either an asyncio task or a thread. I suspect this will solve 90% of the concurrency issues we see with Textual apps.

    See the Worker API for the details.

    "},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/","title":"Textual 0.23.0 improves message handling","text":"

    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\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    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.

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#on-decorator","title":"On decorator","text":"

    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.pyon_decorator02.pyOutput on_decorator01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n        \"\"\"Handle all button pressed events.\"\"\"\n        if event.button.id == \"bell\":\n            self.bell()\n        elif event.button.has_class(\"toggle\", \"dark\"):\n            self.dark = not self.dark\n        elif event.button.id == \"quit\":\n            self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    1. The message handler is called when any button is pressed
    on_decorator02.py
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    @on(Button.Pressed, \"#bell\")  # (1)!\n    def play_bell(self):\n        \"\"\"Called when the bell button is pressed.\"\"\"\n        self.bell()\n\n    @on(Button.Pressed, \".toggle.dark\")  # (2)!\n    def toggle_dark(self):\n        \"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\n        self.dark = not self.dark\n\n    @on(Button.Pressed, \"#quit\")  # (3)!\n    def quit(self):\n        \"\"\"Called when the quit button is pressed.\"\"\"\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    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.

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#why-didnt-we-do-this-earlier","title":"Why didn't we do this earlier?","text":"

    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!

    "},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/","title":"Textual 0.24.0 adds a Select control","text":"

    Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases. At least until it is deposed by version 0.25.0.

    The highlight of this release is the new Select widget: a very familiar control from the web and desktop worlds. Here's a screenshot and code:

    Output (expanded)select_widget.pyselect.css

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select((line, line) for line in LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    \n
    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#new-styles","title":"New styles","text":"

    This one required new functionality in Textual itself. The \"pull-down\" overlay with options presented a difficulty with the previous API. The overlay needed to appear over any content below it. This is possible (using layers), but there was no simple way of positioning it directly under the parent widget.

    We solved this with a new \"overlay\" concept, which can considered a special layer for user interactions like this Select, but also pop-up menus, tooltips, etc. Widgets styled to use the overlay appear in their natural place in the \"document\", but on top of everything else.

    A second problem we tackled was ensuring that an overlay widget was never clipped. This was also solved with a new rule called \"constrain\". Applying constrain to a widget will keep the widget within the bounds of the screen. In the case of Select, if you expand the options while at the bottom of the screen, then the overlay will be moved up so that you can see all the options.

    These new rules are currently undocumented as they are still subject to change, but you can see them in the Select source if you are interested.

    In a future release these will be finalized and you can confidently use them in your own projects.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#fixes-for-the-on-decorator","title":"Fixes for the @on decorator","text":"

    The new @on decorator is proving popular. To recap, it is a more declarative and finely grained way of dispatching messages. Here's a snippet from the calculator example which uses @on:

        @on(Button.Pressed, \"#plus,#minus,#divide,#multiply\")\n    def pressed_op(self, event: Button.Pressed) -> None:\n        \"\"\"Pressed one of the arithmetic operations.\"\"\"\n        self.right = Decimal(self.value or \"0\")\n        self._do_math()\n        assert event.button.id is not None\n        self.operator = event.button.id\n

    The decorator arranges for the method to be called when any of the four math operation buttons are pressed.

    In 0.24.0 we've fixed some missing attributes which prevented the decorator from working with some messages. We've also extended the decorator to use keywords arguments, so it will match attributes other than control.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#other-fixes","title":"Other fixes","text":"

    There is a surprising number of fixes in this release for just 5 days. See CHANGELOG.md for details.

    "},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#join-us","title":"Join us","text":"

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

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/","title":"Textual adds Sparklines, Selection list, Input validation, and tool tips","text":"

    It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.

    We've been a little distracted with our \"dogfood\" projects: Frogmouth and Trogon. Both of which hit 1000 Github stars in 24 hours. We will be maintaining / updating those, but it is business as usual for this Textual release (and it's a big one). We have such sights to show you.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#sparkline-widget","title":"Sparkline widget","text":"

    A Sparkline is essentially a mini-plot. Just detailed enough to keep an eye on time-series data.

    SparklineColorsApp \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582

    Colors are configurable, and all it takes is a call to set_interval to make it animate.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#selection-list","title":"Selection list","text":"

    Next up is the SelectionList widget. Essentially a scrolling list of checkboxes. Lots of use cases for this one.

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#tooltips","title":"Tooltips","text":"

    We've added tooltips to Textual widgets.

    The API couldn't be simpler: simply assign a string to the tooltip property on any widget. This string will be displayed after 300ms when you hover over the widget.

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    As always, you can configure how the tooltips will be displayed with CSS.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#input-updates","title":"Input updates","text":"

    We have some quality of life improvements for the Input widget.

    You can now use a simple declarative API to validating input.

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258afoo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e ['Must\u00a0be\u00a0a\u00a0valid\u00a0number.',\u00a0'Value\u00a0is\u00a0not\u00a0even.',\u00a0\"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\"]

    Also in this release is a suggestion API, which will suggest auto completions as you type. Hit right to accept the suggestion.

    Here's a screenshot:

    FruitsApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258astrawberry\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    You could use this API to offer suggestions from a fixed list, or even pull the data from a network request.

    "},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#join-us","title":"Join us","text":"

    Development on Textual is fast. We're very responsive to issues and feature requests.

    If you have any suggestions, jump on our Discord server and you may see your feature in the next release!

    "},{"location":"blog/2023/07/03/textual-0290-refactors-dev-tools/","title":"Textual 0.29.0 refactors dev tools","text":"

    It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to get a new version out.

    Version 0.29.0 has shipped with a number of fixes (see the release notes for details), but I'd like to use this post to explain a change we made to how Textual developer tools are distributed.

    Previously if you installed textual[dev] you would get the Textual dev tools plus the library itself. If you were distributing Textual apps and didn't need the developer tools you could drop the [dev].

    We did this because the less dependencies a package has, the fewer installation issues you can expect to get in the future. And Textual is surprisingly lean if you only need to run apps, and not build them.

    Alas, this wasn't quite as elegant solution as we hoped. The dependencies defined in extras wouldn't install commands, so textual was bundled with the core library. This meant that if you installed the Textual package without the [dev] you would still get the textual command on your path but it wouldn't run.

    We solved this by creating two packages: textual contains the core library (with minimal dependencies) and textual-dev contains the developer tools. If you are building Textual apps, you should install both as follows:

    pip install textual textual-dev\n

    That's the only difference. If you run in to any issues feel free to ask on the Discord server!

    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/","title":"Textual 0.30.0 adds desktop-style notifications","text":"

    We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.

    By sheer coincidence we reached 20,000 stars on GitHub today. Now stars don't mean all that much (at least until we can spend them on coffee), but its nice to know that twenty thousand developers thought Textual was interesting enough to hit the \u2605 button. Thank you!

    In other news: we moved office. We are now a stone's throw away from Edinburgh Castle. The office is around three times as big as the old place, which means we have room for wide standup desks and dual monitors. But more importantly we have room for new employees. Don't send your CVs just yet, but we hope to grow the team before the end of the year.

    Exciting times.

    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#new-release","title":"New Release","text":"

    And now, for the main feature. Version 0.30 adds a new notification system. Similar to desktop notifications, it displays a small window with a title and message (called a toast) for a pre-defined number of seconds.

    Notifications are great for short timely messages to add supplementary information for the user. Here it is in action:

    The API is super simple. To display a notification, call notify() with a message and an optional title.

    def on_mount(self) -> None:\n    self.notify(\"Hello, from Textual!\", title=\"Welcome\")\n
    "},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#textualize-video-channel","title":"Textualize Video Channel","text":"

    In case you missed it; Textualize now has a YouTube channel. Our very own Rodrigo has recorded a video tutorial series on how to build Textual apps. Check it out!

    We will be adding more videos in the near future, covering anything from beginner to advanced topics.

    Don't worry if you prefer reading to watching videos. We will be adding plenty more content to the Textual docs in the near future. Watch this space.

    As always, if you want to discuss anything with the Textual developers, join us on the Discord server.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/","title":"Textual 0.38.0 adds a syntax aware TextArea","text":"

    This is the second big feature release this month after last week's command palette.

    The TextArea has finally landed. I know a lot of folk have been waiting for this one. Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers. It is highly configurable, and looks great.

    Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea. See Things I learned while building Textual's TextArea for some of the challenges he faced.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#scoped-css","title":"Scoped CSS","text":"

    Another notable feature added in 0.38.0 is scoped CSS. A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget.

    Consider the following widget:

    class MyWidget(Widget):\n    DEFAULT_CSS = \"\"\"\n    MyWidget {\n        height: auto;\n        border: magenta;\n    }\n    Label {\n        border: solid green;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"foo\")\n        yield Label(\"bar\")\n

    The author has intended to style the labels in that widget by adding a green border. This does work for the widget in question, but (prior to 0.38.0) the Label rule would style all Labels (including any outside of the widget) \u2014 which was probably not intended.

    With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled. This is almost always what you want, which is why it is enabled by default. If you do want to style something outside of the widget you can set SCOPED_CSS=False (as a classvar).

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#light-and-dark-pseudo-selectors","title":"Light and Dark pseudo selectors","text":"

    We've also made a slight quality of life improvement to the CSS, by adding :light and :dark pseudo selectors. This allows you to change styles depending on whether you have dark mode enabled or not.

    This was possible before, just a little verbose. Here's how you would do it in 0.37.0:

    App.-dark-mode MyWidget Label {\n    ...\n}\n

    In 0.38.0 it's a little more concise and readable:

    MyWidget:dark Label {\n    ...\n}\n
    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#testing-guide","title":"Testing guide","text":"

    Not strictly part of the release, but we've added a guide on testing Textual apps.

    As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential. We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin pytest-textual-snapshot.

    This gives devs powerful tools to ensure the quality of their apps. Let us know your thoughts on that!

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#release-notes","title":"Release notes","text":"

    See the release page for the full details on this release.

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#whats-next","title":"What's next?","text":"

    There's lots of features planned over the next few months. One feature I am particularly excited by is a widget to generate plots by wrapping the awesome Plotext library. Check out some early work on this feature:

    "},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#join-us","title":"Join us","text":"

    Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

    "},{"location":"blog/2022/11/08/version-040/","title":"Version 0.4.0","text":"

    We've released version 0.4.0 of Textual.

    As this is the first post tagged with release let me first explain where the blog fits in with releases. We plan on doing a post for every note-worthy release. Which likely means all but the most trivial updates (typos just aren't that interesting). Blog posts will be supplementary to release notes which you will find on the Textual repository.

    Blog posts will give a little more background for the highlights in a release, and a rationale for changes and new additions. We embrace building in public, which means that we would like you to be as up-to-date with new developments as if you were sitting in our office. It's a small office, and you might not be a fan of the Scottish weather (it's dreich), but you can at least be here virtually.

    Release 0.4.0 follows 0.3.0, released on October 31st. Here are the highlights of the update.

    "},{"location":"blog/2022/11/08/version-040/#updated-mount-method","title":"Updated Mount Method","text":"

    The mount method has seen some work. We've dropped the ability to assign an id via keyword attributes, which wasn't terribly useful. Now, an id must be assigned via the constructor.

    The mount method has also grown before and after parameters which tell Textual where to add a new Widget (the default was to add it to the end). Here are a few examples:

    # Mount at the start\nself.mount(Button(id=\"Buy Coffee\"), before=0)\n\n# Mount after a selector\nself.mount(Static(\"Password is incorrect\"), after=\"Dialog Input.-error\")\n\n# Mount after a specific widget\ntweet = self.query_one(\"Tweet\")\nself.mount(Static(\"Consider switching to Mastodon\"), after=tweet)\n

    Textual needs much of the same kind of operations as the JS API exposed by the browser. But we are determined to make this way more intuitive. The new mount method is a step towards that.

    "},{"location":"blog/2022/11/08/version-040/#faster-updates","title":"Faster Updates","text":"

    Textual now writes to stdout in a thread. The upshot of this is that Textual can work on the next update before the terminal has displayed the previous frame.

    This means smoother updates all round! You may notice this when scrolling and animating, but even if you don't, you will have more CPU cycles to play with in your Textual app.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ca3Oa3Fx1MDAxNsff91NkPG8r3fdLZ86cybW5t401SXPmmVx1MDAwZUFUXCKK4WIunX73Z0FsXHUwMDAwI2pcdTAwMTKDTPLCKlx1MDAxYvZebNaP/1prQ39/WFurhXdDu/Z5rWbfWqbrtHzzpvYx3j6y/cDxXHUwMDA20ESS34FcdTAwMTf5VrJnN1xmh8HnT5/6pt+zw6FrWrYxcoLIdIMwajmeYXn9T05o94P/xZ/HZt/+79Drt0LfSFx1MDAwN6nbLSf0/IexbNfu24MwgN7/XHUwMDBmv9fWfiefXHUwMDE563zbXG7NQce1k1x1MDAwM5Km1ECK1eTWY2+QXHUwMDE4SzDDVFx1MDAxMk7Q41x1MDAxZU6wXHUwMDA144V2XHUwMDBimttgs522xJtq55fW8dVgOzo971xuWd857UaDzct02Lbjuo3wzn2YXG7T6kZ+xqgg9L2efea0wi6044ntj8dcdTAwMDVcdTAwMWXMQnqU70Wd7sBcdTAwMGWC3DHe0LSc8C7ehlLzXHUwMDFmZuHzWrrlXHUwMDE2filuKClcdTAwMTVcdTAwMTVCYc6kXHUwMDE0j63J8ZxcdTAwMWGEU41cdTAwMDThXGL+Ju3a9FxcuFx1MDAxNGDXf6hgbUumll2aVq9cdTAwMDPmXHJa6T6EqEtbKJXudfP3fJkyiJSUUclcdTAwMWZcdTAwMWK7ttPphtDKkKGlXCJgI5KYXHUwMDExRVMr7ORqxC2USMZcdTAwMWVcdTAwMWLioYd7rcQx/pmczK7pXHUwMDBmx5NWXHUwMDBi4lx1MDAxZlx1MDAxObNji7cnvSrrWZlcdTAwMGJ+2T5qXHUwMDFjOMdcciFcdTAwMWPinp9uXHUwMDFmXHUwMDA218HPx75yblx1MDAxONq3Ye2x4c/H6d3m9v646IBpt+Nv6Vx0R8OW+eCwWFxiqWGKJaY09Vx1MDAwMNdcdTAwMTn0oHFcdTAwMTC5brrNs3qpj3/I2PtMuDQthotKXHUwMDA03pa5YPPgut9ccr5aR0PyvVlf92+PLbp/9l1UXHUwMDFkrjp4q0GRXHUwMDE2XHUwMDA0cy01opzk8FwiXGJcdTAwMWKEUnBuJqTChFx1MDAxN+LF27Rlsdl42YJphabhRVxiM4RmkjMmgFx1MDAxZq5ewFx1MDAxONxcdTAwMGaVwHBcdTAwMDaqXFzIml/I/p55MrpAw9b1tT/sbl37J29cdNn0XHUwMDAxn1x1MDAwM1x1MDAxOUwkUrxcdTAwMTTIXHUwMDE4xkWQYSm4RIhxsTBkw1x1MDAxZt++7l1cdTAwMWW1aGRcdTAwMWTgId9wej/vulWHjFx0XHUwMDBljGFcdTAwMDJcdTAwMTOviKSgXCJcdTAwMTNcdTAwMTImXGbMlVx1MDAxNFx1MDAxOCeuzYohW6mGYaQg2lx1MDAxMFx1MDAxNJNy+dq17qXd22t2b2lvyLqX9uHoXHUwMDFifku+plx1MDAwZlhRXHUwMDExY1pcdTAwMTbxJZhcIkhgtLiG3e2qKPq1dVx1MDAxZlx1MDAxYzXNkfn91t05cdpVx4tQaVx1MDAwMFVcdTAwMTju/pJzzSfo0spA0FwiXHUwMDE555RLnLnZVE/CXGLinCnKsChcdTAwMTexg63v6Ng+6nxcdTAwMGbCbvO62d7mqH/2lohNXHUwMDFmcHWI5ezMJoikXGIuTFx1MDAxOIeQnujFxWt2oFBRuupCXHUwMDE4TIBzM1xuai2QyONFkDZcYokjyPiCKKqXXHUwMDExIT6FKyOZjzRlg9ExPlx1MDAwMJVcdTAwMTBYv4VCLTOaSi+5N1xiXHUwMDFizn2ScaDc1lx1MDAxZLPvuHe5q5b4aOxGyfjZeVxmbFx1MDAxODNxSpXbe911OrFcdTAwMTfXLDhcdTAwMGLbzzl46Fim+7hD32m1svpigVx0JvTp7y2S23i+03FcdTAwMDam+yNv4YuIK6x3MKmpQGRxMZud/FZcdTAwMTQ3TJmBsODgUFxcQFx1MDAwMqry+Vx1MDAxOFx1MDAxNpCPMVxu07BENXuKWyZcdTAwMGKcgVx1MDAxYqaEXHUwMDBirbF8XHUwMDAztVrmnf91uJ35Tsm0zSnTTdL2YOCLYCvOzTjHXHUwMDFhJluphXGbXHUwMDFkQ1RcdTAwMTQ3iqnBXHUwMDE1Y5prXHUwMDA0mZGarH5cYlx1MDAwM0muYFx1MDAxZYA1ls18ViFuXHUwMDEwNCrMdbniVjJtK1x1MDAxMLc5SU8p4iYgZ6BU4cWDydlZcUVx44BcdTAwMWKjWsPpclx1MDAxOVdCJsSNXHUwMDE5XHUwMDE4JE/qh1xmSqo3wW1BcVx1MDAwMyvB0pJDyXevbXNcbngv0LbZ1ZHMZE4gJ1x0QpqpZ1x1MDAwNJT7I3SAdjZa/kE/XHUwMDFhnUdHfp1wXXXkMKKGXHUwMDAy5WJcYkI1hjjOIcewNFx1MDAwNChcdTAwMWZCY5ErJG7FpUfQZ1x1MDAwMVx1MDAwMW/JdVx1MDAxMeF87TaO2DpB9+bJSe8wwvhm9Jq6yFx1MDAxYnU7r9wyfcC02/G3alQ0OS1cXJbjRGhCsmvAc2Wy8+XQPJDfnOZF65c2zZsrc++u6szWMUlcdTAwMTa9OTi8oFwiXHUwMDBlTfPUXG6QUak4pH/JqncxtasvaWKCQEhFdpGnXHUwMDE0dCN2u3s9ijbcXHUwMDAzT9zwweHVcc/uvVx1MDAxZd2ldztcdTAwMGbd6Vx1MDAwM1ZcdTAwMTVdrYvQJZpoXHIh7uJye9A7adCGa91ftK9+7Gy2T5v3vdOqo0spMTAnXHUwMDAwLlx1MDAxMVxmXCJZzfLkYmFcYlx1MDAxZcdcdTAwMWRcdTAwMGbAZG51lVx1MDAxMlxcolC85l82tPvWyW5k4u0uP6xcdTAwMWaMblx1MDAwNlx1MDAwM1Cr6PXQLr3bedBOXHUwMDFmsKLQSsSLoMVcdTAwMWHFSal+XHUwMDA2tdH2VXR4tyt/ycPG9lx1MDAxNW9IsnfxperUYsRcZsqFwpJSXHUwMDExL2boPLWgt5CfYyrwPGorILiSY1x1MDAxNa9Olczu1sXXy0NN7ky457Gj4O6871xmL1/P7tK7ncfu9Fx1MDAwMVfHbmHtVlx1MDAxND9YoyA4XHUwMDA0aJ+R286OayqKbZ0jXHUwMDAzUYRcdTAwMTVcdTAwMDI448WSiYdDmcRcdTAwMDZcdTAwMTNSUPqQ3L7Rasli9du4sqWIxO+7orSC+u2cXHUwMDE0b5n1W4xcdTAwMGLjWy21ZOCKiyM3u1xuUFHkMMeGjlx1MDAxN/0gxGVxOWmCOEhcXLlcdTAwMDYh1VgnXG62yvXJeMVcdTAwMDRcdKrouyau/Fx1MDAxYe6cOuhcdTAwMTLXJ8lcZt5cdTAwMTSDm756xuM3syOJqvKmkEFcdTAwMTDDXHUwMDFh4jnIx8hEOlx0XHUwMDAyJyghT+PF8vVNQ85cdTAwMGKRqX7XXHUwMDBmXHUwMDAzrEDe5iRUS5U3XZxcdFIpiFr8VaPZKXdFYaMxbCBtTGGqp1x1MDAxNG/i7IwhJMRY2+Sb4Lbg+iRcdTAwMDfkJc247jvErXxtm1N0XFyitlx1MDAxNT5cdTAwMGKAXHUwMDA1XCKCaJEpy8yj7eJLdK+Q02tcXHX3XHUwMDFhO1dnW0PvdLfqtGFFXGZcdTAwMTRnZVx1MDAwNCdcdTAwMGZcdTAwMDPofCippVx1MDAwMUmdYip+qlx1MDAxM9LZV7FWXFwoXHUwMDE1U2osT2mjXHUwMDA0bn/wUX4kSXRptG3Ybc9/XHUwMDFlbq7dXHUwMDBlZ8BcdTAwMTZ6w1wi0nJnMYnV2JJcdTAwMTdxVfg6XHUwMDA0ZkzHIUqmejePK9usb1x1MDAwNH3xrd+MzlG9j++3tjevK8+VIIZcdTAwMDBqXG64ojxeoVx1MDAxMFxuKVx1MDAxND9BOut1o9eRlcn9ZpAlKKVSKVa+jpVI1no7J0qrXHUwMDAz68GQmVxcmb7v3UxNxlDhujxcdTAwMDQjSHKK9OJoscb53k7n+HJ9py72/P3dU7y19cKHaSafynzDV424QWAvTjWjkIzhiXSMXCJlcFx1MDAwMOLvXHUwMDAzbK9Lx4rJgsFccqD872NymE5J0LChMFx1MDAwNKtiSuSIOFFcdTAwMWGJXHUwMDE3vIueWFg2cUFo+uGGM2g5g87kIfagVdDimkG46fX7TlxiZnzznEE4uUfS73rs7V3bfEJcdTAwMGX0nG2rhb4zsYI2jDvNL4uk39ZSz0l+PH7/5+PUvVx0llx1MDAwNlI4juhxPreI/+rI0EhcdTAwMDGAmPB5PVx1MDAxNTtH0lPqXHUwMDE3aUdcdTAwMWay/z5Xb0XxS/SIKU3gfNLLPrdGc/Szuf+r88VcdTAwMTXO6c9r+/asqe2rqt9cdTAwMTXqXFxcdTAwMTlcdTAwMTBcdTAwMWRcblx1MDAxZf+PXHUwMDFjQiOevytAKG9gzcZcdTAwMDXRV69CzLgtLCS4mFx0grR8529t/HD6lYhkXHUwMDEzO1x1MDAxZbj6MEa2Zlx1MDAwZYeNMC7SfFx1MDAxZVNcdTAwMDZcdTAwMTfAaY1PMe2tNnLsm40pXHUwMDFl0E7+4l5cdTAwMTNWYyrsePp///nw51+TJY25In0= UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime"},{"location":"blog/2022/11/08/version-040/#multiple-css-paths","title":"Multiple CSS Paths","text":"

    Up to version 0.3.0, Textual would only read a single CSS file set in the CSS_PATH class variable. You can now supply a list of paths if you have more than one CSS file.

    This change was prompted by tuilwindcss which brings a TailwindCSS like approach to building Textual Widgets. Also check out calmcode.io by the same author, which is an amazing resource.

    "},{"location":"blog/2022/12/11/version-060/","title":"Textual 0.6.0 adds a treemendous new widget","text":"

    A new release of Textual lands 3 weeks after the previous release -- and it's a big one.

    Information

    If you're new here, Textual is TUI framework for Python.

    "},{"location":"blog/2022/12/11/version-060/#tree-control","title":"Tree Control","text":"

    The headline feature of version 0.6.0 is a new tree control built from the ground-up. The previous Tree control suffered from an overly complex API and wasn't scalable (scrolling slowed down with 1000s of nodes).

    This new version has a simpler API and is highly scalable (no slowdown with larger trees). There are also a number of visual enhancements in this version.

    Here's a very simple example:

    Outputtree.py

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\n\n\nclass TreeApp(App):\n    def compose(self) -> ComposeResult:\n        tree: Tree[dict] = Tree(\"Dune\")\n        tree.root.expand()\n        characters = tree.root.add(\"Characters\", expand=True)\n        characters.add_leaf(\"Paul\")\n        characters.add_leaf(\"Jessica\")\n        characters.add_leaf(\"Chani\")\n        yield tree\n\n\nif __name__ == \"__main__\":\n    app = TreeApp()\n    app.run()\n

    Here's the tree control being used to navigate some JSON (json_tree.py in the examples directory).

    I'm biased of course, but I think this terminal based tree control is more usable (and even prettier) than just about anything I've seen on the web or desktop. So much of computing tends to organize itself in to a tree that I think this widget will find a lot of uses.

    The Tree control forms the foundation of the DirectoryTree widget, which has also been updated. Here it is used in the code_browser.py example:

    "},{"location":"blog/2022/12/11/version-060/#list-view","title":"List View","text":"

    We have a new ListView control to navigate and select items in a list. Items can be widgets themselves, which makes this a great platform for building more sophisticated controls.

    Outputlist_view.pylist_view.css

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    \n
    "},{"location":"blog/2022/12/11/version-060/#placeholder","title":"Placeholder","text":"

    The Placeholder widget was broken since the big CSS update. We've brought it back and given it a bit of a polish.

    Use this widget in place of custom widgets you have yet to build when designing your UI. The colors are automatically cycled to differentiate one placeholder from the next. You can click a placeholder to cycle between its ID, size, and lorem ipsum text.

    Outputplaceholder.pyplaceholder.css

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3 #p5Placeholder Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0 Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0 33\u00a0x\u00a011nec\u00a0libero\u00a0quis\u00a0gravida.\u00a034\u00a0x\u00a011 Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0 Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0 sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0 amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus50\u00a0x\u00a011 sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis. Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibusimperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedsit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapienlacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass PlaceholderApp(App):\n    CSS_PATH = \"placeholder.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield VerticalScroll(\n            Container(\n                Placeholder(\"This is a custom label for p1.\", id=\"p1\"),\n                Placeholder(\"Placeholder p2 here!\", id=\"p2\"),\n                Placeholder(id=\"p3\"),\n                Placeholder(id=\"p4\"),\n                Placeholder(id=\"p5\"),\n                Placeholder(),\n                Horizontal(\n                    Placeholder(variant=\"size\", id=\"col1\"),\n                    Placeholder(variant=\"text\", id=\"col2\"),\n                    Placeholder(variant=\"size\", id=\"col3\"),\n                    id=\"c1\",\n                ),\n                id=\"bot\",\n            ),\n            Container(\n                Placeholder(variant=\"text\", id=\"left\"),\n                Placeholder(variant=\"size\", id=\"topright\"),\n                Placeholder(variant=\"text\", id=\"botright\"),\n                id=\"top\",\n            ),\n            id=\"content\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = PlaceholderApp()\n    app.run()\n
    \n
    "},{"location":"blog/2022/12/11/version-060/#fixes","title":"Fixes","text":"

    As always, there are a number of fixes in this release. Mostly related to layout. See CHANGELOG.md for the details.

    "},{"location":"blog/2022/12/11/version-060/#whats-next","title":"What's next?","text":"

    The next release will focus on pain points we discovered while in a dog-fooding phase (see the DevLog for details on what Textual devs have been building).

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/","title":"Textual 0.37.0 adds a command palette","text":"

    Textual version 0.37.0 has landed! The highlight of this release is the new command palette.

    A command palette gives users quick access to features in your app. If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands. The commands are matched with a fuzzy search, so you only need to type two or three characters to get to any command.

    Here's a video of it in action:

    Adding your own commands to the command palette is a piece of cake. Here's the (command) Provider class used in the example above:

    class ColorCommands(Provider):\n    \"\"\"A command provider to select colors.\"\"\"\n\n    async def search(self, query: str) -> Hits:\n        \"\"\"Called for each key.\"\"\"\n        matcher = self.matcher(query)\n        for color in COLOR_NAME_TO_RGB.keys():\n            score = matcher.match(color)\n            if score > 0:\n                yield Hit(\n                    score,\n                    matcher.highlight(color),\n                    partial(self.app.post_message, SwitchColor(color)),\n                )\n

    And here is how you add a provider to your app:

    class ColorApp(App):\n    \"\"\"Experiment with the command palette.\"\"\"\n\n    COMMANDS = App.COMMANDS | {ColorCommands}\n

    We're excited about this feature because it is a step towards bringing a common user interface to Textual apps.

    Quote

    It's a Textual app. I know this.

    \u2014 You, maybe.

    The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all.

    See the Guide for details on how to work with the command palette.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#what-else","title":"What else?","text":"

    Also in 0.37.0 we have a new Collapsible widget, which is a great way of adding content while avoiding a cluttered screen.

    And of course, bug fixes and other updates. See the release page for the full details.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#whats-next","title":"What's next?","text":"

    Coming very soon, is a new TextEditor widget. This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages. We're expecting that to land next week. Watch this space, or join the Discord server if you want to be the first to try it out.

    "},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#join-us","title":"Join us","text":"

    Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/","title":"Letting your cook multitask while bringing water to a boil","text":"

    Whenever you are cooking a time-consuming meal, you want to multitask as much as possible. For example, you do not want to stand still while you wait for a pot of water to start boiling. Similarly, you want your applications to remain responsive (i.e., you want the cook to \u201cmultitask\u201d) while they do some time-consuming operations in the background (e.g., while the water heats up).

    The animation below shows an example of an application that remains responsive (colours on the left still change on click) even while doing a bunch of time-consuming operations (shown on the right).

    In this blog post, I will teach you how to multitask like a good cook.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#wasting-time-staring-at-pots","title":"Wasting time staring at pots","text":"

    There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve. Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file. The first time I had to do something like this, I ended up writing an application that \u201cblocked\u201d. This means that while the application was reading and parsing the data, nothing else worked.

    To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data. After the data is ready, we display a Label on the right that says that the data has been loaded. On the left, the app has a big rectangle (a custom widget called ColourChanger) that you can click and that changes background colours randomly.

    When you start the application, you can click the rectangle on the left to change the background colour of the ColourChanger, as the animation below shows:

    However, as soon as you press l to trigger the data loading process, clicking the ColourChanger widget doesn't do anything. The app doesn't respond because it is busy working on the data. This is the code of the app so you can try it yourself:

    import time\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):  # (1)!\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]  # (2)!\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:  # (3)!\n        time.sleep(5)  # (4)!\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\n\n\nMyApp().run()\n
    1. The widget ColourChanger changes colours, randomly, when clicked.
    2. We create a binding to the key l that runs an action that we know will take some time (for example, reading and parsing a huge file).
    3. The method action_load is responsible for starting our time-consuming task and then reporting back.
    4. To simplify things a bit, our \u201ctime-consuming task\u201d is just standing still for 5 seconds.

    I think it is easy to understand why the widget ColourChanger stops working when we hit the time.sleep call if we consider the cooking analogy I have written about before in my blog. In short, Python behaves like a lone cook in a kitchen:

    • the cook can be clever and multitask. For example, while water is heating up and being brought to a boil, the cook can go ahead and chop some vegetables.
    • however, there is only one cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.

    Things like \u201cchopping up vegetables\u201d and \u201cseasoning a salad\u201d are blocking, i.e., they need the cook's time and attention. In the app that I showed above, the call to time.sleep is blocking, so the cook can't go and do anything else until the time interval elapses.

    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#how-can-a-cook-multitask","title":"How can a cook multitask?","text":"

    It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook. Python is like a very dumb cook who only ever does one thing at a time and waits until each thing is completely done before doing the next thing. So, by default, Python would act like a cook who fills up a pan with water, starts heating the water, and then stands there staring at the water until it starts boiling instead of doing something else. It is by using the module asyncio from the standard library that our cook learns to do other tasks while awaiting the completion of the things they already started doing.

    Textual is an async framework, which means it knows how to interoperate with the module asyncio and this will be the solution to our problem. By using asyncio with the tasks we want to run in the background, we will let the application remain responsive while we load and parse the data we need, or while we crunch the numbers we need to crunch, or while we connect to some slow API over the Internet, or whatever it is you want to do.

    The module asyncio uses the keyword async to know which functions can be run asynchronously. In other words, you use the keyword async to identify functions that contain tasks that would otherwise force the cook to waste time. (Functions with the keyword async are called coroutines.)

    The module asyncio also introduces a function asyncio.create_task that you can use to run coroutines concurrently. So, if we create a coroutine that is in charge of doing the time-consuming operation and then run it with asyncio.create_task, we are well on our way to fix our issues.

    However, the keyword async and asyncio.create_task alone aren't enough. Consider this modification of the previous app, where the method action_load now uses asyncio.create_task to run a coroutine who does the sleeping:

    import asyncio\nimport time\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:  # (1)!\n        asyncio.create_task(self._do_long_operation())  # (2)!\n\n    async def _do_long_operation(self) -> None:  # (3)!\n        time.sleep(5)\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\n\n\nMyApp().run()\n
    1. The action method action_load now defers the heavy lifting to another method we created.
    2. The time-consuming operation can be run concurrently with asyncio.create_task because it is a coroutine.
    3. The method _do_long_operation has the keyword async, so it is a coroutine.

    This modified app also works but it suffers from the same issue as the one before! The keyword async tells Python that there will be things inside that function that can be awaited by the cook. That is, the function will do some time-consuming operation that doesn't require the cook's attention. However, we need to tell Python which time-consuming operation doesn't require the cook's attention, i.e., which time-consuming operation can be awaited, with the keyword await.

    Whenever we want to use the keyword await, we need to do it with objects that are compatible with it. For many things, that means using specialised libraries:

    • instead of time.sleep, one can use await asyncio.sleep;
    • instead of the module requests to make Internet requests, use aiohttp; or
    • instead of using the built-in tools to read files, use aiofiles.
    "},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#achieving-good-multitasking","title":"Achieving good multitasking","text":"

    To fix the last example application, all we need to do is replace the call to time.sleep with a call to asyncio.sleep and then use the keyword await to signal Python that we can be doing something else while we sleep. The animation below shows that we can still change colours while the application is completing the time-consuming operation.

    CodeAnimation
    import asyncio\nfrom random import randint\n\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\n\n\nclass ColourChanger(Widget):\n    def on_click(self) -> None:\n        self.styles.background = Color(\n            randint(1, 255),\n            randint(1, 255),\n            randint(1, 255),\n        )\n\n\nclass MyApp(App[None]):\n    BINDINGS = [(\"l\", \"load\", \"Load data\")]\n    CSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            ColourChanger(),\n            VerticalScroll(id=\"log\"),\n        )\n        yield Footer()\n\n    def action_load(self) -> None:\n        asyncio.create_task(self._do_long_operation())\n\n    async def _do_long_operation(self) -> None:\n        self.query_one(\"#log\").mount(Label(\"Starting \u23f3\"))  # (1)!\n        await asyncio.sleep(5)  # (2)!\n        self.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))  # (3)!\n\n\nMyApp().run()\n
    1. We create a label that tells the user that we are starting our time-consuming operation.
    2. We await the time-consuming operation so that the application remains responsive.
    3. We create a label that tells the user that the time-consuming operation has been concluded.

    Because our time-consuming operation runs concurrently, everything else in the application still works while we await for the time-consuming operation to finish. In particular, we can keep changing colours (like the animation above showed) but we can also keep activating the binding with the key l to start multiple instances of the same time-consuming operation! The animation below shows just this:

    Warning

    The animation GIFs in this blog post show low-quality colours in an attempt to reduce the size of the media files you have to download to be able to read this blog post. If you run Textual locally you will see beautiful colours \u2728

    "},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/","title":"Using Rich Inspect to interrogate Python objects","text":"

    The Rich library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is inspect which is so useful you may want to pip install rich just for this feature.

    The easiest way to describe inspect is that it is Python's builtin help() but easier on the eye (and with a few more features). If you invoke it with any object, inspect will display a nicely formatted report on that object \u2014 which makes it great for interrogating objects from the REPL. Here's an example:

    >>> from rich import inspect\n>>> text_file = open(\"foo.txt\", \"w\")\n>>> inspect(text_file)\n

    Here we're inspecting a file object, but it could be literally anything. You will see the following output in the terminal:

    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    By default, inspect will generate a data-oriented summary with a text representation of the object and its data attributes. You can also add methods=True to show all the methods in the public API. Here's an example:

    >>> inspect(text_file, methods=True)\n
    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502detach\u00a0=def\u00a0detach():Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502fileno\u00a0=def\u00a0fileno():Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502flush\u00a0=def\u00a0flush():Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502isatty\u00a0=def\u00a0isatty():Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502readable\u00a0=def\u00a0readable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):Change\u00a0stream\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502writable\u00a0=def\u00a0writable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    The documentation is summarized by default to avoid generating verbose reports. If you want to see the full unabbreviated help you can add help=True:

    >>> inspect(text_file, methods=True, help=True)\n
    Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502encoding\u00a0gives\u00a0the\u00a0name\u00a0of\u00a0the\u00a0encoding\u00a0that\u00a0the\u00a0stream\u00a0will\u00a0be\u2502 \u2502decoded\u00a0or\u00a0encoded\u00a0with.\u00a0It\u00a0defaults\u00a0to\u00a0locale.getencoding().\u2502 \u2502\u2502 \u2502errors\u00a0determines\u00a0the\u00a0strictness\u00a0of\u00a0encoding\u00a0and\u00a0decoding\u00a0(see\u2502 \u2502help(codecs.Codec)\u00a0or\u00a0the\u00a0documentation\u00a0for\u00a0codecs.register)\u00a0and\u2502 \u2502defaults\u00a0to\u00a0\"strict\".\u2502 \u2502\u2502 \u2502newline\u00a0controls\u00a0how\u00a0line\u00a0endings\u00a0are\u00a0handled.\u00a0It\u00a0can\u00a0be\u00a0None,\u00a0'',\u2502 \u2502'\\n',\u00a0'\\r',\u00a0and\u00a0'\\r\\n'.\u00a0\u00a0It\u00a0works\u00a0as\u00a0follows:\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0input,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0universal\u00a0newlines\u00a0mode\u00a0is\u2502 \u2502\u00a0\u00a0enabled.\u00a0Lines\u00a0in\u00a0the\u00a0input\u00a0can\u00a0end\u00a0in\u00a0'\\n',\u00a0'\\r',\u00a0or\u00a0'\\r\\n',\u00a0and\u2502 \u2502\u00a0\u00a0these\u00a0are\u00a0translated\u00a0into\u00a0'\\n'\u00a0before\u00a0being\u00a0returned\u00a0to\u00a0the\u2502 \u2502\u00a0\u00a0caller.\u00a0If\u00a0it\u00a0is\u00a0'',\u00a0universal\u00a0newline\u00a0mode\u00a0is\u00a0enabled,\u00a0but\u00a0line\u2502 \u2502\u00a0\u00a0endings\u00a0are\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u00a0If\u00a0it\u00a0has\u00a0any\u00a0of\u2502 \u2502\u00a0\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0input\u00a0lines\u00a0are\u00a0only\u00a0terminated\u00a0by\u00a0the\u00a0given\u2502 \u2502\u00a0\u00a0string,\u00a0and\u00a0the\u00a0line\u00a0ending\u00a0is\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0output,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u2502 \u2502\u00a0\u00a0translated\u00a0to\u00a0the\u00a0system\u00a0default\u00a0line\u00a0separator,\u00a0os.linesep.\u00a0If\u2502 \u2502\u00a0\u00a0newline\u00a0is\u00a0''\u00a0or\u00a0'\\n',\u00a0no\u00a0translation\u00a0takes\u00a0place.\u00a0If\u00a0newline\u00a0is\u00a0any\u2502 \u2502\u00a0\u00a0of\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u00a0translated\u2502 \u2502\u00a0\u00a0to\u00a0the\u00a0given\u00a0string.\u2502 \u2502\u2502 \u2502If\u00a0line_buffering\u00a0is\u00a0True,\u00a0a\u00a0call\u00a0to\u00a0flush\u00a0is\u00a0implied\u00a0when\u00a0a\u00a0call\u00a0to\u2502 \u2502write\u00a0contains\u00a0a\u00a0newline\u00a0character.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():\u2502 \u2502Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502\u2502 \u2502This\u00a0method\u00a0has\u00a0no\u00a0effect\u00a0if\u00a0the\u00a0file\u00a0is\u00a0already\u00a0closed.\u2502 \u2502detach\u00a0=def\u00a0detach():\u2502 \u2502Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502\u2502 \u2502After\u00a0the\u00a0underlying\u00a0buffer\u00a0has\u00a0been\u00a0detached,\u00a0the\u00a0TextIO\u00a0is\u00a0in\u00a0an\u2502 \u2502unusable\u00a0state.\u2502 \u2502fileno\u00a0=def\u00a0fileno():\u2502 \u2502Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502\u2502 \u2502OSError\u00a0is\u00a0raised\u00a0if\u00a0the\u00a0IO\u00a0object\u00a0does\u00a0not\u00a0use\u00a0a\u00a0file\u00a0descriptor.\u2502 \u2502flush\u00a0=def\u00a0flush():\u2502 \u2502Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502\u2502 \u2502This\u00a0is\u00a0not\u00a0implemented\u00a0for\u00a0read-only\u00a0and\u00a0non-blocking\u00a0streams.\u2502 \u2502isatty\u00a0=def\u00a0isatty():\u2502 \u2502Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502\u2502 \u2502Return\u00a0False\u00a0if\u00a0it\u00a0can't\u00a0be\u00a0determined.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):\u2502 \u2502Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502\u2502 \u2502Read\u00a0from\u00a0underlying\u00a0buffer\u00a0until\u00a0we\u00a0have\u00a0n\u00a0characters\u00a0or\u00a0we\u00a0hit\u00a0EOF.\u2502 \u2502If\u00a0n\u00a0is\u00a0negative\u00a0or\u00a0omitted,\u00a0read\u00a0until\u00a0EOF.\u2502 \u2502readable\u00a0=def\u00a0readable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0read()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):\u2502 \u2502Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502\u2502 \u2502Returns\u00a0an\u00a0empty\u00a0string\u00a0if\u00a0EOF\u00a0is\u00a0hit\u00a0immediately.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):\u2502 \u2502Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502\u2502 \u2502hint\u00a0can\u00a0be\u00a0specified\u00a0to\u00a0control\u00a0the\u00a0number\u00a0of\u00a0lines\u00a0read:\u00a0no\u00a0more\u2502 \u2502lines\u00a0will\u00a0be\u00a0read\u00a0if\u00a0the\u00a0total\u00a0size\u00a0(in\u00a0bytes/characters)\u00a0of\u00a0all\u2502 \u2502lines\u00a0so\u00a0far\u00a0exceeds\u00a0hint.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):\u2502 \u2502Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502\u2502 \u2502This\u00a0also\u00a0does\u00a0an\u00a0implicit\u00a0stream\u00a0flush.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):\u2502 \u2502Change\u00a0stream\u00a0position.\u2502 \u2502\u2502 \u2502Change\u00a0the\u00a0stream\u00a0position\u00a0to\u00a0the\u00a0given\u00a0byte\u00a0offset.\u00a0The\u00a0offset\u00a0is\u2502 \u2502interpreted\u00a0relative\u00a0to\u00a0the\u00a0position\u00a0indicated\u00a0by\u00a0whence.\u00a0\u00a0Values\u2502 \u2502for\u00a0whence\u00a0are:\u2502 \u2502\u2502 \u2502*\u00a00\u00a0--\u00a0start\u00a0of\u00a0stream\u00a0(the\u00a0default);\u00a0offset\u00a0should\u00a0be\u00a0zero\u00a0or\u00a0positive\u2502 \u2502*\u00a01\u00a0--\u00a0current\u00a0stream\u00a0position;\u00a0offset\u00a0may\u00a0be\u00a0negative\u2502 \u2502*\u00a02\u00a0--\u00a0end\u00a0of\u00a0stream;\u00a0offset\u00a0is\u00a0usually\u00a0negative\u2502 \u2502\u2502 \u2502Return\u00a0the\u00a0new\u00a0absolute\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0seek(),\u00a0tell()\u00a0and\u00a0truncate()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502This\u00a0method\u00a0may\u00a0need\u00a0to\u00a0do\u00a0a\u00a0test\u00a0seek().\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):\u2502 \u2502Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502\u2502 \u2502File\u00a0pointer\u00a0is\u00a0left\u00a0unchanged.\u00a0\u00a0Size\u00a0defaults\u00a0to\u00a0the\u00a0current\u00a0IO\u2502 \u2502position\u00a0as\u00a0reported\u00a0by\u00a0tell().\u00a0\u00a0Returns\u00a0the\u00a0new\u00a0size.\u2502 \u2502writable\u00a0=def\u00a0writable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0write()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):\u2502 \u2502Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2502\u2502 \u2502Line\u00a0separators\u00a0are\u00a0not\u00a0added,\u00a0so\u00a0it\u00a0is\u00a0usual\u00a0for\u00a0each\u00a0of\u00a0the\u2502 \u2502lines\u00a0provided\u00a0to\u00a0have\u00a0a\u00a0line\u00a0separator\u00a0at\u00a0the\u00a0end.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    There are a few more arguments to refine the level of detail you need (private methods, dunder attributes etc). You can see the full range of options with this delightful little incantation:

    >>> inspect(inspect)\n

    If you are interested in Rich or Textual, join our Discord server!

    "},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/#addendum","title":"Addendum","text":"

    Here's how to have inspect always available without an explicit import:

    Put this in your pythonrc file: pic.twitter.com/pXTi69ykZL

    \u2014 Tushar Sadhwani (@sadhlife) July 27, 2023"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/","title":"Spinners and progress bars in Textual","text":"

    One of the things I love about mathematics is that you can solve a problem just by guessing the correct answer. That is a perfectly valid strategy for solving a problem. The only thing you need to do after guessing the answer is to prove that your guess is correct.

    I used this strategy, to some success, to display spinners and indeterminate progress bars from Rich in Textual.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-an-indeterminate-progress-bar-in-textual","title":"Display an indeterminate progress bar in Textual","text":"

    I have been playing around with Textual and recently I decided I needed an indeterminate progress bar to show that some data was loading. Textual is likely to get progress bars in the future, but I don't want to wait for the future! I want my progress bars now! Textual builds on top of Rich, so if Rich has progress bars, I reckoned I could use them in my Textual apps.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#progress-bars-in-rich","title":"Progress bars in Rich","text":"

    Creating a progress bar in Rich is as easy as opening up the documentation for Progress and copying & pasting the code.

    CodeOutput
    import time\nfrom rich.progress import track\n\nfor _ in track(range(20), description=\"Processing...\"):\n    time.sleep(0.5)  # Simulate work being done\n

    The function track provides a very convenient interface for creating progress bars that keep track of a well-specified number of steps. In the example above, we were keeping track of some task that was going to take 20 steps to complete. (For example, if we had to process a list with 20 elements.) However, I am looking for indeterminate progress bars.

    Scrolling further down the documentation for rich.progress I found what I was looking for:

    CodeOutput
    import time\nfrom rich.progress import Progress\n\nwith Progress() as progress:\n    _ = progress.add_task(\"Loading...\", total=None)  # (1)!\n    while True:\n        time.sleep(0.01)\n
    1. Setting total=None is what makes it an indeterminate progress bar.

    So, putting an indeterminate progress bar on the screen is easy. Now, I only needed to glue that together with the little I know about Textual to put an indeterminate progress bar in a Textual app.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#guessing-what-is-what-and-what-goes-where","title":"Guessing what is what and what goes where","text":"

    What I want is to have an indeterminate progress bar inside my Textual app. Something that looks like this:

    The GIF above shows just the progress bar. Obviously, the end goal is to have the progress bar be part of a Textual app that does something.

    So, when I set out to do this, my first thought went to the stopwatch app in the Textual tutorial because it has a widget that updates automatically, the TimeDisplay. Below you can find the essential part of the code for the TimeDisplay widget and a small animation of it updating when the stopwatch is started.

    TimeDisplay widgetOutput
    from time import monotonic\n\nfrom textual.reactive import reactive\nfrom textual.widgets import Static\n\n\nclass TimeDisplay(Static):\n    \"\"\"A widget to display elapsed time.\"\"\"\n\n    start_time = reactive(monotonic)\n    time = reactive(0.0)\n    total = reactive(0.0)\n\n    def on_mount(self) -> None:\n        \"\"\"Event handler called when widget is added to the app.\"\"\"\n        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\n\n    def update_time(self) -> None:\n        \"\"\"Method to update time to current.\"\"\"\n        self.time = self.total + (monotonic() - self.start_time)\n\n    def watch_time(self, time: float) -> None:\n        \"\"\"Called when the time attribute changes.\"\"\"\n        minutes, seconds = divmod(time, 60)\n        hours, minutes = divmod(minutes, 60)\n        self.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n

    The reason the time display updates magically is due to the three methods that I highlighted in the code above:

    1. The method on_mount is called when the TimeDisplay widget is mounted on the app and, in it, we use the method set_interval to let Textual know that every 1 / 60 seconds we would like to call the method update_time. (In other words, we would like update_time to be called 60 times per second.)
    2. In turn, the method update_time (which is called automatically a bunch of times per second) will update the reactive attribute time. When this attribute update happens, the method watch_time kicks in.
    3. The method watch_time is a watcher method and gets called whenever the attribute self.time is assigned to. So, if the method update_time is called a bunch of times per second, the watcher method watch_time is also called a bunch of times per second. In it, we create a nice representation of the time that has elapsed and we use the method update to update the time that is being displayed.

    I thought it would be reasonable if a similar mechanism needed to be in place for my progress bar, but then I realised that the progress bar seems to update itself... Looking at the indeterminate progress bar example from before, the only thing going on was that we used time.sleep to stop our program for a bit. We didn't do anything to update the progress bar... Look:

    with Progress() as progress:\n    _ = progress.add_task(\"Loading...\", total=None)  # (1)!\n    while True:\n        time.sleep(0.01)\n

    After pondering about this for a bit, I realised I would not need a watcher method for anything. The watcher method would only make sense if I needed to update an attribute related to some sort of artificial progress, but that clearly isn't needed to get the bar going...

    At some point, I realised that the object progress is the object of interest. At first, I thought progress.add_task would return the progress bar, but it actually returns the integer ID of the task added, so the object of interest is progress. Because I am doing nothing to update the bar explicitly, the object progress must be updating itself.

    The Textual documentation also says that we can build widgets from Rich renderables, so I concluded that if Progress were a renderable, then I could inherit from Static and use the method update to update the widget with my instance of Progress directly. I gave it a try and I put together this code:

    from rich.progress import Progress, BarColumn\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass IndeterminateProgress(Static):\n    def __init__(self):\n        super().__init__(\"\")\n        self._bar = Progress(BarColumn())  # (1)!\n        self._bar.add_task(\"\", total=None)  # (2)!\n\n    def on_mount(self) -> None:\n        # When the widget is mounted start updating the display regularly.\n        self.update_render = self.set_interval(\n            1 / 60, self.update_progress_bar\n        )  # (3)!\n\n    def update_progress_bar(self) -> None:\n        self.update(self._bar)  # (4)!\n\n\nclass MyApp(App):\n    def compose(self) -> ComposeResult:\n        yield IndeterminateProgress()\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    1. Create an instance of Progress that just cares about the bar itself (Rich progress bars can have a label, an indicator for the time left, etc).
    2. We add the indeterminate task with total=None for the indeterminate progress bar.
    3. When the widget is mounted on the app, we want to start calling update_progress_bar 60 times per second.
    4. To update the widget of the progress bar we just call the method Static.update with the Progress object because self._bar is a Rich renderable.

    And lo and behold, it worked:

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#proving-it-works","title":"Proving it works","text":"

    I finished writing this piece of code and I was ecstatic because it was working! After all, my Textual app starts and renders the progress bar. And so, I shared this simple app with someone who wanted to do a similar thing, but I was left with a bad taste in my mouth because I couldn't really connect all the dots and explain exactly why it worked.

    Plot twist

    By the end of the blog post, I will be much closer to a full explanation!

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-a-rich-spinner-in-a-textual-app","title":"Display a Rich spinner in a Textual app","text":"

    A day after creating my basic IndeterminateProgress widget, I found someone that was trying to display a Rich spinner in a Textual app. Actually, it was someone that had filed an issue against Rich. They didn't ask \u201chow can I display a Rich spinner in a Textual app?\u201d, but they filed an alleged bug that crept up on them when they tried displaying a spinner in a Textual app.

    When reading the issue I realised that displaying a Rich spinner looked very similar to displaying a Rich progress bar, so I made a tiny change to my code and tried to run it:

    CodeSpinner running
    from rich.spinner import Spinner\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass SpinnerWidget(Static):\n    def __init__(self):\n        super().__init__(\"\")\n        self._spinner = Spinner(\"moon\")  # (1)!\n\n    def on_mount(self) -> None:\n        self.update_render = self.set_interval(1 / 60, self.update_spinner)\n\n    def update_spinner(self) -> None:\n        self.update(self._spinner)\n\n\nclass MyApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield SpinnerWidget()\n\n\nMyApp().run()\n
    1. Instead of creating an instance of Progress, we create an instance of Spinner and save it so we can call self.update(self._spinner) later on.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#losing-the-battle-against-pausing-the-animations","title":"Losing the battle against pausing the animations","text":"

    After creating the progress bar and spinner widgets I thought of creating the little display that was shown at the beginning of the blog post:

    When writing the code for this app, I realised both widgets had a lot of shared code and logic and I tried abstracting away their common functionality. That led to the code shown below (more or less) where I implemented the updating functionality in IntervalUpdater and then let the IndeterminateProgressBar and SpinnerWidget instantiate the correct Rich renderable.

    from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\n\nfrom textual.app import RenderableType\nfrom textual.widgets import Button, Static\n\n\nclass IntervalUpdater(Static):\n    _renderable_object: RenderableType  # (1)!\n\n    def update_rendering(self) -> None:  # (2)!\n        self.update(self._renderable_object)\n\n    def on_mount(self) -> None:  # (3)!\n        self.interval_update = self.set_interval(1 / 60, self.update_rendering)\n\n\nclass IndeterminateProgressBar(IntervalUpdater):\n    \"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Progress(BarColumn())  # (4)!\n        self._renderable_object.add_task(\"\", total=None)\n\n\nclass SpinnerWidget(IntervalUpdater):\n    \"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\n    def __init__(self, style: str) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Spinner(style)  # (5)!\n
    1. Instances of IntervalUpdate should set the attribute _renderable_object to the instance of the Rich renderable that we want to animate.
    2. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
    3. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
    4. For an indeterminate progress bar we set the attribute _renderable_object to an instance of Progress.
    5. For a spinner we set the attribute _renderable_object to an instance of Spinner.

    But I wanted something more! I wanted to make my app similar to the stopwatch app from the terminal and thus wanted to add a \u201cPause\u201d and a \u201cResume\u201d button. These buttons should, respectively, stop the progress bar and the spinner animations and resume them.

    Below you can see the code I wrote and a short animation of the app working.

    App codeCSSOutput
    from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\n\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Grid, Horizontal, Vertical\nfrom textual.widgets import Button, Static\n\n\nclass IntervalUpdater(Static):\n    _renderable_object: RenderableType\n\n    def update_rendering(self) -> None:\n        self.update(self._renderable_object)\n\n    def on_mount(self) -> None:\n        self.interval_update = self.set_interval(1 / 60, self.update_rendering)\n\n    def pause(self) -> None:  # (1)!\n        self.interval_update.pause()\n\n    def resume(self) -> None:  # (2)!\n        self.interval_update.resume()\n\n\nclass IndeterminateProgressBar(IntervalUpdater):\n    \"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Progress(BarColumn())\n        self._renderable_object.add_task(\"\", total=None)\n\n\nclass SpinnerWidget(IntervalUpdater):\n    \"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\n    def __init__(self, style: str) -> None:\n        super().__init__(\"\")\n        self._renderable_object = Spinner(style)\n\n\nclass LiveDisplayApp(App[None]):\n    \"\"\"App showcasing some widgets that update regularly.\"\"\"\n    CSS_PATH = \"myapp.css\"\n\n    def compose(self) -> ComposeResult:\n        yield Vertical(\n                Grid(\n                    SpinnerWidget(\"moon\"),\n                    IndeterminateProgressBar(),\n                    SpinnerWidget(\"aesthetic\"),\n                    SpinnerWidget(\"bouncingBar\"),\n                    SpinnerWidget(\"earth\"),\n                    SpinnerWidget(\"dots8Bit\"),\n                ),\n                Horizontal(\n                    Button(\"Pause\", id=\"pause\"),  # (3)!\n                    Button(\"Resume\", id=\"resume\", disabled=True),\n                ),\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (4)!\n        pressed_id = event.button.id\n        assert pressed_id is not None\n        for widget in self.query(IntervalUpdater):\n            getattr(widget, pressed_id)()  # (5)!\n\n        for button in self.query(Button):  # (6)!\n            if button.id == pressed_id:\n                button.disabled = True\n            else:\n                button.disabled = False\n\n\nLiveDisplayApp().run()\n
    1. The method pause looks at the attribute interval_update (returned by the method set_interval) and tells it to stop calling the method update_rendering 60 times per second.
    2. The method resume looks at the attribute interval_update (returned by the method set_interval) and tells it to resume calling the method update_rendering 60 times per second.
    3. We set two distinct IDs for the two buttons so we can easily tell which button was pressed and what the press of that button means.
    4. The event handler on_button_pressed will wait for button presses and will take care of pausing or resuming the animations.
    5. We look for all of the instances of IntervalUpdater in our app and use a little bit of introspection to call the correct method (pause or resume) in our widgets. Notice this was only possible because the buttons were assigned IDs that matched the names of the methods. (I love Python !)
    6. We go through our two buttons to disable the one that was just pressed and to enable the other one.
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    height: 1fr;\n    align-horizontal: center;\n}\n\nButton {\n    margin: 0 3 0 3;\n}\n\nGrid {\n    height: 4fr;\n    align: center middle;\n    grid-size: 3 2;\n    grid-columns: 8;\n    grid-rows: 1;\n    grid-gutter: 1;\n    border: gray double;\n}\n\nIntervalUpdater {\n    content-align: center middle;\n}\n

    If you think this was a lot, take a couple of deep breaths before moving on.

    The only issue with my app is that... it does not work! If you press the button to pause the animations, it looks like the widgets are paused. However, you can see that if I move my mouse over the paused widgets, they update:

    Obviously, that caught me by surprise, in the sense that I expected it work. On the other hand, this isn't surprising. After all, I thought I had guessed how I could solve the problem of displaying these Rich renderables that update live and I thought I knew how to pause and resume their animations, but I hadn't convinced myself I knew exactly why it worked.

    Warning

    This goes to show that sometimes it is not the best idea to commit code that you wrote and that works if you don't know why it works. The code might seem to work and yet have deficiencies that will hurt you further down the road.

    As it turns out, the reason why pausing is not working is that I did not grok why the rendering worked in the first place... So I had to go down that rabbit hole first.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#understanding-the-rich-rendering-magic","title":"Understanding the Rich rendering magic","text":""},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-staticupdate-works","title":"How Static.update works","text":"

    The most basic way of creating a Textual widget is to inherit from Widget and implement the method render that just returns the thing that must be printed on the screen. Then, the widget Static provides some functionality on top of that: the method update.

    The method Static.update(renderable) is used to tell the widget in question that its method render (called when the widget needs to be drawn) should just return renderable. So, if the implementation of the method IntervalUpdater.update_rendering (the method that gets called 60 times per second) is this:

    class IntervalUpdater(Static):\n    # ...\n    def update_rendering(self) -> None:\n        self.update(self._renderable_object)\n

    Then, we are essentially saying \u201chey, the thing in self._renderable_object is what must be returned whenever Textual asks you to render yourself. So, this really proves that both Progress and Spinner from Rich are renderables. But what is more, this shows that my implementation of IntervalUpdater can be simplified greatly! In fact, we can boil it down to just this:

    class IntervalUpdater(Static):\n    _renderable_object: RenderableType\n\n    def __init__(self, renderable_object: RenderableType) -> None:  # (1)!\n        super().__init__(renderable_object)  # (2)!\n\n    def on_mount(self) -> None:\n        self.interval_update = self.set_interval(1 / 60, self.refresh)  # (3)!\n
    1. To create an instance of IntervalUpdater, now we give it the Rich renderable that we want displayed. If this Rich renderable is something that updates over time, then those changes will be reflected in the rendering.
    2. We initialise Static with the renderable object itself, instead of initialising with the empty string \"\" and then updating repeatedly.
    3. We call self.refresh 60 times per second. We don't need the auxiliary method update_rendering because this widget (an instance of Static) already knows what its renderable is.

    Once you understand the code above you will realise that the previous implementation of update_rendering was actually doing superfluous work because the repeated calls to self.update always had the exact same object. Again, we see strong evidence that the Rich progress bars and the spinners have the inherent ability to display a different representation of themselves as time goes by.

    "},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-rich-spinners-get-updated","title":"How Rich spinners get updated","text":"

    I kept seeing strong evidence that Rich spinners and Rich progress bars updated their own rendering but I still did not have actual proof. So, I went digging around to see how Spinner was implemented and I found this code (from the file spinner.py at the time of writing):

    class Spinner:\n    # ...\n\n    def __rich_console__(\n        self, console: \"Console\", options: \"ConsoleOptions\"\n    ) -> \"RenderResult\":\n        yield self.render(console.get_time())  # (1)!\n\n    # ...\n    def render(self, time: float) -> \"RenderableType\":  # (2)!\n        # ...\n\n        frame_no = ((time - self.start_time) * self.speed) / (  # (3)!\n            self.interval / 1000.0\n        ) + self.frame_no_offset\n        # ...\n\n    # ...\n
    1. The Rich spinner implements the function __rich_console__ that is supposed to return the result of rendering the spinner. Instead, it defers its work to the method render... However, to call the method render, we need to pass the argument console.get_time(), which the spinner uses to know in which state it is!
    2. The method render takes a time and returns a renderable!
    3. To determine the frame number (the current look of the spinner) we do some calculations with the \u201ccurrent time\u201d, given by the parameter time, and the time when the spinner started!

    The snippet of code shown above, from the implementation of Spinner, explains why moving the mouse over a spinner (or a progress bar) that supposedly was paused makes it move. We no longer get repeated updates (60 times per second) because we told our app that we wanted to pause the result of set_interval, so we no longer get automatic updates. However, moving the mouse over the spinners and the progress bar makes Textual want to re-render them and, when it does, it figures out that time was not frozen (obviously!) and so the spinners and the progress bar have a different frame to show.

    To get a better feeling for this, do the following experiment:

    1. Run the command textual console in a terminal to open the Textual devtools console.
    2. Add a print statement like print(\"Rendering from within spinner\") to the beginning of the method Spinner.render (from Rich).
    3. Add a print statement like print(\"Rendering static\") to the beginning of the method Static.render (from Textual).
    4. Put a blank terminal and the devtools console side by side.
    5. Run the app: notice that you get a lot of both print statements.
    6. Hit the Pause button: the print statements stop.
    7. Move your mouse over a widget or two: you get a couple of print statements, one from the Static.render and another from the Spinner.render.

    The result of steps 6 and 7 are shown below. Notice that, in the beginning of the animation, the screen on the right shows some prints but is quiet because no more prints are coming in. When the mouse enters the screen and starts going over widgets, the screen on the right gets new prints in pairs, first from Static.render (which Textual calls to render the widget) and then from Spinner.render because ultimately we need to know how the Spinner looks.

    Now, at this point, I made another educated guess and deduced that progress bars work in the same way! I still have to prove it, and I guess I will do so in another blog post, coming soon, where our spinner and progress bar widgets can be properly paused!

    I will see you soon

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/","title":"Stealing Open Source code from Textual","text":"

    I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't steal a car would you?

    But you should steal code from Open Source projects. Respect the license (you may need to give attribution) but stealing code is not like stealing a car. If I steal your car, I have deprived you of a car. If you steal my open source code, I haven't lost anything.

    Warning

    I'm not advocating for piracy. Open source code gives you explicit permission to use it.

    From my point of view, I feel like code has greater value when it has been copied / modified in another project.

    There are a number of files and modules in Textual that could either be lifted as is, or wouldn't require much work to extract. I'd like to cover a few here. You might find them useful in your next project.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#loop-first-last","title":"Loop first / last","text":"

    How often do you find yourself looping over an iterable and needing to know if an element is the first and/or last in the sequence? It's a simple thing, but I find myself needing this a lot, so I wrote some helpers in _loop.py.

    I'm sure there is an equivalent implementation on PyPI, but steal this if you need it.

    Here's an example of use:

    for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):\n    yield move_to(x, y)\n    yield from line\n    if not last:\n        yield new_line\n
    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#lru-cache","title":"LRU Cache","text":"

    Python's lru_cache can be the one-liner that makes your code orders of magnitude faster. But it has a few gotchas.

    The main issue is managing the lifetime of these caches. The decorator keeps a single global cache, which will keep a reference to every object in the function call. On an instance method that means you keep references to self for the lifetime of your app.

    For a more flexibility you can use the LRUCache implementation from Textual. This uses essentially the same algorithm as the stdlib decorator, but it is implemented as a container.

    Here's a quick example of its use. It works like a dictionary until you reach a maximum size. After that, new elements will kick out the element that was used least recently.

    >>> from textual._cache import LRUCache\n>>> cache = LRUCache(maxsize=3)\n>>> cache[\"foo\"] = 1\n>>> cache[\"bar\"] = 2\n>>> cache[\"baz\"] = 3\n>>> dict(cache)\n{'foo': 1, 'bar': 2, 'baz': 3}\n>>> cache[\"egg\"] = 4\n>>> dict(cache)\n{'bar': 2, 'baz': 3, 'egg': 4}\n

    In Textual, we use a LRUCache to store the results of rendering content to the terminal. For example, in a datatable it is too costly to render everything up front. So Textual renders only the lines that are currently visible on the \"screen\". The cache ensures that scrolling only needs to render the newly exposed lines, and lines that haven't been displayed in a while are discarded to save memory.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#color","title":"Color","text":"

    Textual has a Color class which could be extracted in to a module of its own.

    The Color class can parse colors encoded in a variety of HTML and CSS formats. Color object support a variety of methods and operators you can use to manipulate colors, in a fairly natural way.

    Here's some examples in the REPL.

    >>> from textual.color import Color\n>>> color = Color.parse(\"lime\")\n>>> color\nColor(0, 255, 0, a=1.0)\n>>> color.darken(0.8)\nColor(0, 45, 0, a=1.0)\n>>> color + Color.parse(\"red\").with_alpha(0.1)\nColor(25, 229, 0, a=1.0)\n>>> color = Color.parse(\"#12a30a\")\n>>> color\nColor(18, 163, 10, a=1.0)\n>>> color.css\n'rgb(18,163,10)'\n>>> color.hex\n'#12A30A'\n>>> color.monochrome\nColor(121, 121, 121, a=1.0)\n>>> color.monochrome.hex\n'#797979'\n>>> color.hsl\nHSL(h=0.3246187363834423, s=0.8843930635838151, l=0.33921568627450976)\n>>>\n

    There are some very good color libraries in PyPI, which you should also consider using. But Textual's Color class is lean and performant, with no C dependencies.

    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#geometry","title":"Geometry","text":"

    This may be my favorite module in Textual: geometry.py.

    The geometry module contains a number of classes responsible for storing and manipulating 2D geometry. There is an Offset class which is a two dimensional point. A Region class which is a rectangular region defined by a coordinate and dimensions. There is a Spacing class which defines additional space around a region. And there is a Size class which defines the dimensions of an area by its width and height.

    These objects are used by Textual's layout engine and compositor, which makes them the oldest and most thoroughly tested part of the project.

    There's a lot going on in this module, but the docstrings are quite detailed and have unicode art like this to help explain things.

                  cut_x \u2193\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502        \u2502 \u2502   \u2502\n          \u2502    0   \u2502 \u2502 1 \u2502\n          \u2502        \u2502 \u2502   \u2502\n  cut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502    2   \u2502 \u2502 3 \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
    "},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#you-should-steal-our-code","title":"You should steal our code","text":"

    There is a lot going on in the Textual Repository. Including a CSS parser, renderer, layout and compositing engine. All written in pure Python. Steal it with my blessing.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/","title":"Things I learned building a text editor for the terminal","text":"

    TextArea is the latest widget to be added to Textual's growing collection. It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.

    Adding a TextArea to your Textual app is as simple as adding this to your compose method:

    yield TextArea()\n

    Enabling syntax highlighting for a language is as simple as:

    yield TextArea(language=\"python\")\n

    Working on the TextArea widget for Textual taught me a lot about Python and my general approach to software engineering. It gave me an appreciation for the subtle functionality behind the editors we use on a daily basis \u2014 features we may not even notice, despite some engineer spending hours perfecting it to provide a small boost to our development experience.

    This post is a tour of some of these learnings.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#vertical-cursor-movement-is-more-than-just-cursor_row","title":"Vertical cursor movement is more than just cursor_row++","text":"

    When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. Editors should maintain the visual column offset where possible, meaning they must account for double-width emoji (sigh \ud83d\ude14) and East-Asian characters.

    Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it arrives at line 3. This is because the 6th character of line 3 visually aligns with the 11th character of line 1.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-from-other-sources-may-move-my-cursor","title":"Edits from other sources may move my cursor","text":"

    There are two ways to interact with the TextArea:

    1. You can type into it.
    2. You can make API calls to edit the content in it.

    In the example below, Hello, world!\\n is repeatedly inserted at the start of the document via the API. Notice that this updates the location of my cursor, ensuring that I don't lose my place.

    This subtle feature should aid those implementing collaborative and multi-cursor editing.

    This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result.

    Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way!

    A TetrisArea white-boarding session.

    Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks Dave!) is what's needed to finally crack a tough problem.

    Many thanks to David Brochart for sending me down this rabbit hole!

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#spending-a-few-minutes-running-a-profiler-can-be-really-beneficial","title":"Spending a few minutes running a profiler can be really beneficial","text":"

    While building the TextArea widget I avoided heavy optimisation work that may have affected readability or maintainability.

    However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were affecting the performance of my code.

    I spent around 30 minutes profiling TextArea using pyinstrument, and the result was a ~97% reduction in the time taken to handle a key press. What an amazing return on investment for such a minimal time commitment!

    \"pyinstrument -r html\" produces this beautiful output.

    pyinstrument unveiled two issues that were massively impacting performance.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#1-reparsing-highlighting-queries-on-each-key-press","title":"1. Reparsing highlighting queries on each key press","text":"

    I was constructing a tree-sitter Query object on each key press, incorrectly assuming it was a low-overhead call. This query was completely static, so I moved it into the constructor ensuring the object was created only once. This reduced key processing time by around 94% - a substantial and very much noticeable improvement.

    This seems obvious in hindsight, but the code in question was written earlier in the project and had been relegated in my mind to \"code that works correctly and will receive less attention from here on out\". pyinstrument quickly brought this code back to my attention and highlighted it as a glaring performance bug.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#2-namedtuples-are-slower-than-i-expected","title":"2. NamedTuples are slower than I expected","text":"

    In Python, NamedTuples are slow to create relative to tuples, and this cost was adding up inside an extremely hot loop which was instantiating a large number of them. pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside NamedTuple.__new__.

    Here's a quick benchmark which constructs 10,000 NamedTuples:

    \u276f hyperfine -w 2 'python sandbox/darren/make_namedtuples.py'\nBenchmark 1: python sandbox/darren/make_namedtuples.py\n  Time (mean \u00b1 \u03c3):      15.9 ms \u00b1   0.5 ms    [User: 12.8 ms, System: 2.5 ms]\n  Range (min \u2026 max):    15.2 ms \u2026  18.4 ms    165 runs\n

    Here's the same benchmark using tuple instead:

    \u276f hyperfine -w 2 'python sandbox/darren/make_tuples.py'\nBenchmark 1: python sandbox/darren/make_tuples.py\n  Time (mean \u00b1 \u03c3):       9.3 ms \u00b1   0.5 ms    [User: 6.8 ms, System: 2.0 ms]\n  Range (min \u2026 max):     8.7 ms \u2026  12.3 ms    256 runs\n

    Switching to tuple resulted in another noticeable increase in responsiveness. Key-press handling time dropped by almost 50%! Unfortunately, this change does impact readability. However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#syntax-highlighting-is-very-different-from-what-i-expected","title":"Syntax highlighting is very different from what I expected","text":"

    In order to support syntax highlighting, we make use of the tree-sitter library, which maintains a syntax tree representing the structure of our document.

    To perform highlighting, we follow these steps:

    1. The user edits the document.
    2. We inform tree-sitter of the location of this edit.
    3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree.
    4. We run a query against the tree to retrieve ranges of text we wish to highlight.
    5. These ranges are mapped to styles (defined by the chosen \"theme\").
    6. These styles to the appropriate text ranges when rendering the widget.

    Cycling through a few of the builtin themes.

    Another benefit that I didn't consider before working on this project is that tree-sitter parsers can also be used to highlight syntax errors in a document. This can be useful in some situations - for example, highlighting mismatched HTML closing tags:

    Highlighting mismatched closing HTML tags in red.

    Before building this widget, I was oblivious as to how we might approach syntax highlighting. Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have been feasible.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-are-replacements","title":"Edits are replacements","text":"

    All single-cursor edits can be distilled into a single behaviour: replace_range. This replaces a range of characters with some text. We can use this one method to easily implement deletion, insertion, and replacement of text.

    • Inserting text is replacing a zero-width range with the text to insert.
    • Pressing backspace (delete left) is just replacing the character behind the cursor with an empty string.
    • Selecting text and pressing delete is just replacing the selected text with an empty string.
    • Selecting text and pasting is replacing the selected text with some other text.

    This greatly simplified my initial approach, which involved unique implementations for inserting and deleting.

    "},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#the-line-between-text-area-and-vscode-in-the-terminal","title":"The line between \"text area\" and \"VSCode in the terminal\"","text":"

    A project like this has no clear finish line. There are always new features, optimisations, and refactors waiting to be made.

    So where do we draw the line?

    We want to provide a widget which can act as both a basic multiline text area that anyone can drop into their app, yet powerful and extensible enough to act as the foundation for a Textual-powered text editor.

    Yet, the more features we add, the more opinionated the widget becomes, and the less that users will feel like they can build it into their own thing. Finding the sweet spot between feature-rich and flexible is no easy task.

    I don't think the answer is clear, and I don't believe it's possible to please everyone.

    Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using TextArea in the future!

    "},{"location":"blog/2023/10/04/announcing-textual-plotext/","title":"Announcing textual-plotext","text":"

    It's no surprise that a common question on the Textual Discord server is how to go about producing plots in the terminal. A popular solution that has been suggested is Plotext. While Plotext doesn't directly support Textual, it is easy to use with Rich and, because of this, we wanted to make it just as easy to use in your Textual applications.

    With this in mind we've created textual-plotext: a library that provides a widget for using Plotext plots in your app. In doing this we've tried our best to make it as similar as possible to using Plotext in a conventional Python script.

    Take this code from the Plotext README:

    import plotext as plt\ny = plt.sin() # sinusoidal test signal\nplt.scatter(y)\nplt.title(\"Scatter Plot\") # to apply a title\nplt.show() # to finally plot\n

    The Textual equivalent of this (including everything needed to make this a fully-working Textual application) is:

    from textual.app import App, ComposeResult\n\nfrom textual_plotext import PlotextPlot\n\nclass ScatterApp(App[None]):\n\n    def compose(self) -> ComposeResult:\n        yield PlotextPlot()\n\n    def on_mount(self) -> None:\n        plt = self.query_one(PlotextPlot).plt\n        y = plt.sin() # sinusoidal test signal\n        plt.scatter(y)\n        plt.title(\"Scatter Plot\") # to apply a title\n\nif __name__ == \"__main__\":\n    ScatterApp().run()\n

    When run the result will look like this:

    Aside from a couple of the more far-out plot types1 you should find that everything you can do with Plotext in a conventional script can also be done in a Textual application.

    Here's a small selection of screenshots from a demo built into the library, each of the plots taken from the Plotext README:

    A key design goal of this widget is that you can develop your plots so that the resulting code looks very similar to that in the Plotext documentation. The core difference is that, where you'd normally import the plotext module as plt and then call functions via plt, you instead use the plt property made available by the widget.

    You don't even need to call the build or show functions as textual-plotext takes care of this for you. You can see this in action in the scatter code shown earlier.

    Of course, moving any existing plotting code into your Textual app means you will need to think about how you get the data and when and where you build your plot. This might be where the Textual worker API becomes useful.

    We've included a longer-form example application that shows off the glorious Scottish weather we enjoy here at Textual Towers, with an application that uses workers to pull down weather data from a year ago and plot it.

    If you are an existing Plotext user who wants to turn your plots into full terminal applications, we think this will be very familiar and accessible. If you're a Textual user who wants to add plots to your application, we think Plotext is a great library for this.

    If you have any questions about this, or anything else to do with Textual, feel free to come and join us on our Discord server or in our GitHub discussions.

    1. Right now there's no animated gif or video support.\u00a0\u21a9

    "},{"location":"blog/2023/09/06/what-is-textual-web/","title":"What is Textual Web?","text":"

    If you know us, you will know that we are the team behind Rich and Textual \u2014 two popular Python libraries that work magic in the terminal.

    Note

    Not to mention Rich-CLI, Trogon, and Frogmouth

    Today we are adding one project more to that lineup: textual-web.

    Textual Web takes a Textual-powered TUI and turns it in to a web application. Here's a video of that in action:

    With the textual-web command you can publish any Textual app on the web, making it available to anyone you send the URL to. This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications.

    We're excited about the possibilities here. Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. They can be built by a single developer without any experience with a traditional web stack. All you need is proficiency in Python and a little time to read our lovely docs.

    Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. We plan to do this in a way that allows the same (Python) code to drive those features. For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser.

    Also in the pipeline is PWA support, so you can build terminal apps, web apps, and desktop apps with a single codebase.

    Textual Web is currently in a public beta. Join our Discord server if you would like to help us test, or if you have any questions.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/","title":"To TUI or not to TUI","text":"

    Tech moves pretty fast. If you don\u2019t stop and look around once in a while, you could miss it. And yet some technology feels like it has been around forever.

    Terminals are one of those forever-technologies.

    My interest is in Text User Interfaces: interactive apps that run within a terminal. I spend lot of time thinking about where TUIs might fit within the tech ecosystem, and how much more they could be doing for developers. Hardly surprising, since that is what we do at Textualize.

    Recently I had the opportunity to test how new TUI projects would be received. You can consider these to be \"testing the water\", and hopefully representative of TUI apps in general.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#the-projects","title":"The projects","text":"

    In April we took a break from building Textual, to building apps with Textual. We had three ideas to work on, and three devs to do the work. One idea we parked for later. The other two were so promising we devoted more time to them. Both projects took around three developer-weeks to build, which also included work on Textual itself and standard duties for responding to issues / community requests. We released them in May.

    The first project was Frogmouth, a Markdown browser. I think this TUI does better than the equivalent web experience in many ways. The only notable missing feature is images, and that will happen before too long.

    Here's a screenshot:

    Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

    Info

    Quick aside about these \"screenshots\", because its a common ask. They aren't true screenshots, but rather SVGs exported by Textual.

    We posted Frogmouth on Hacker News and Reddit on a Sunday morning (US time). A day later, it had 1,000 stars and lots of positive feedback.

    The second project was Trogon, a library this time. Trogon automatically creates a TUI for command line apps. Same deal: we released it on a Sunday morning, and it reached 1K stars even quicker than Frogmouth.

    Trogon sqlite-utilstransform v3.31Transform\u00a0a\u00a0table\u00a0beyond\u00a0the\u00a0capabilities\u00a0of\u00a0ALTER\u00a0TABLE \u258a\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258e disable-wal\u258a\u258a\u258e\u258e drop-table\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e drop-view\u258a\u258e\u2587\u2587 dump\u258aOptions\u258e duplicate\u258a\u258e enable-counts\u258a--type\u00a0multiple\u00a0<text\u00a0choice>\u258e enable-fts\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e enable-wal\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e extract\u258a\u2502\u258a\u258e\u2502\u258e\u2585\u2585 index-foreign-keys\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e indexes\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e insert\u258a\u2502\u258aSelect\u25b2\u258e\u2502\u258e insert-files\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e install\u258a\u2514\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2518\u258e memory\u258a\u258aSelect\u258e\u258e optimize\u258a\u258aINTEGER\u258e\u258e populate-fts\u258a\u258aTEXT\u258e\u258e query\u258a\u258aFLOAT\u258e\u258e rebuild-fts\u258a\u258a\u258aBLOB\u258e\u258e\u258e reset-counts\u258a\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e\u258e rows\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e schema\u258a+\u00a0value\u258e search\u258aDrop\u00a0this\u00a0column\u258e tables\u258a\u258e transform\u258a--rename\u00a0multiple\u00a0<text\u00a0text>\u258e triggers\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e uninstall\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e upsert\u258a\u2502\u258a\u258e\u2502\u258e vacuum views$\u00a0sqlite-utils\u00a0transform \u00a0CTRL+R\u00a0\u00a0Close\u00a0&\u00a0Run\u00a0\u00a0CTRL+T\u00a0Focus\u00a0Command\u00a0Tree\u00a0\u00a0CTRL+O\u00a0\u00a0Command\u00a0Info\u00a0\u00a0CTRL+S\u00a0\u00a0Search\u00a0\u00a0F1\u00a0\u00a0About\u00a0

    Both of these projects are very young, but off to a great start. I'm looking forward to seeing how far we can taken them.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#wrapping-up","title":"Wrapping up","text":"

    With previous generations of software, TUIs have required a high degree of motivation to build. That has changed with the work that we (and others) have been doing. A TUI can be a powerful and maintainable piece of software which works as a standalone project, or as a value-add to an existing project.

    As a forever-technology, a TUI is a safe bet.

    "},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#discord","title":"Discord","text":"

    Want to discuss this post with myself or other Textualize devs? Join our Discord server...

    "},{"location":"css_types/","title":"CSS Types","text":"

    CSS types define the values that Textual CSS styles accept.

    CSS types will be linked from within the styles reference in the \"Formal Syntax\" section of each style. The CSS types will be denoted by a keyword enclosed by angle brackets < and >.

    For example, the style align-horizontal references the CSS type <horizontal>:

    \nalign-horizontal: <horizontal>;\n
    "},{"location":"css_types/border/","title":"<border>","text":"

    The <border> CSS type represents a border style.

    "},{"location":"css_types/border/#syntax","title":"Syntax","text":"

    The <border> type can take any of the following values:

    Border type Description ascii A border with plus, hyphen, and vertical bar characters. blank A blank border (reserves space for a border). dashed Dashed line border. double Double lined border. heavy Heavy border. hidden Alias for \"none\". hkey Horizontal key-line border. inner Thick solid border. none Disabled border. outer Solid border with additional space around content. panel Solid border with thick top. round Rounded corners. solid Solid border. tall Solid border with additional space top and bottom. thick Border style that is consistently thick across edges. vkey Vertical key-line border. wide Solid border with additional space left and right."},{"location":"css_types/border/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

    textual borders\n
    "},{"location":"css_types/border/#examples","title":"Examples","text":""},{"location":"css_types/border/#css","title":"CSS","text":"
    #container {\n    border: heavy red;\n}\n\n#heading {\n    border-bottom: solid blue;\n}\n
    "},{"location":"css_types/border/#python","title":"Python","text":"
    widget.styles.border = (\"heavy\", \"red\")\nwidget.styles.border_bottom = (\"solid\", \"blue\")\n
    "},{"location":"css_types/color/","title":"<color>","text":"

    The <color> CSS type represents a color.

    Warning

    Not to be confused with the color CSS rule to set text color.

    "},{"location":"css_types/color/#syntax","title":"Syntax","text":"

    A <color> should be in one of the formats explained in this section. A bullet point summary of the formats available follows:

    • a recognised named color (e.g., red);
    • a 3 or 6 hexadecimal digit number representing the RGB values of the color (e.g., #F35573);
    • a 4 or 8 hexadecimal digit number representing the RGBA values of the color (e.g., #F35573A0);
    • a color description in the RGB system, with or without opacity (e.g., rgb(23, 78, 200));
    • a color description in the HSL system, with or without opacity (e.g., hsl(290, 70%, 80%));

    Textual's default themes also provide many CSS variables with colors that can be used out of the box.

    "},{"location":"css_types/color/#named-colors","title":"Named colors","text":"

    A named color is a <name> that Textual recognises. Below, you can find a (collapsed) list of all of the named colors that Textual recognises, along with their hexadecimal values, their RGB values, and a visual sample.

    All named colors available. colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"css_types/color/#hex-rgb-value","title":"Hex RGB value","text":"

    The hexadecimal RGB format starts with an octothorpe # and is then followed by 3 or 6 hexadecimal digits: 0123456789ABCDEF. Casing is ignored.

    • If 6 digits are used, the format is #RRGGBB:
    • RR represents the red channel;
    • GG represents the green channel; and
    • BB represents the blue channel.
    • If 3 digits are used, the format is #RGB.

    In a 3 digit color, each channel is represented by a single digit which is duplicated when converting to the 6 digit format. For example, the color #A2F is the same as #AA22FF.

    "},{"location":"css_types/color/#hex-rgba-value","title":"Hex RGBA value","text":"

    This is the same as the hex RGB value, but with an extra channel for the alpha component (that sets opacity).

    • If 8 digits are used, the format is #RRGGBBAA, equivalent to the format #RRGGBB with two extra digits for opacity.
    • If 4 digits are used, the format is #RGBA, equivalent to the format #RGB with an extra digit for opacity.
    "},{"location":"css_types/color/#rgb-description","title":"rgb description","text":"

    The rgb format description is a functional description of a color in the RGB color space. This description follows the format rgb(red, green, blue), where red, green, and blue are decimal integers between 0 and 255. They represent the value of the channel with the same name.

    For example, rgb(0, 255, 32) is equivalent to #00FF20.

    "},{"location":"css_types/color/#rgba-description","title":"rgba description","text":"

    The rgba format description is the same as the rgb with an extra parameter for opacity, which should be a value between 0 and 1.

    For example, rgba(0, 255, 32, 0.5) is the color rgb(0, 255, 32) with 50% opacity.

    "},{"location":"css_types/color/#hsl-description","title":"hsl description","text":"

    The hsl format description is a functional description of a color in the HSL color space. This description follows the format hsl(hue, saturation, lightness), where

    • hue is a float between 0 and 360;
    • saturation is a percentage between 0% and 100%; and
    • lightness is a percentage between 0% and 100%.

    For example, the color #00FF20 would be represented as hsl(128, 100%, 50%) in the HSL color space.

    "},{"location":"css_types/color/#hsla-description","title":"hsla description","text":"

    The hsla format description is the same as the hsl with an extra parameter for opacity, which should be a value between 0 and 1.

    For example, hsla(128, 100%, 50%, 0.5) is the color hsl(128, 100%, 50%) with 50% opacity.

    "},{"location":"css_types/color/#examples","title":"Examples","text":""},{"location":"css_types/color/#css","title":"CSS","text":"
    Header {\n    background: red;           /* Color name */\n}\n\n.accent {\n    color: $accent;            /* Textual variable */\n}\n\n#footer {\n    tint: hsl(300, 20%, 70%);  /* HSL description */\n}\n
    "},{"location":"css_types/color/#python","title":"Python","text":"

    In Python, rules that expect a <color> can also accept an instance of the type Color.

    # Mimicking the CSS syntax\nwidget.styles.background = \"red\"           # Color name\nwidget.styles.color = \"$accent\"            # Textual variable\nwidget.styles.tint = \"hsl(300, 20%, 70%)\"  # HSL description\n\nfrom textual.color import Color\n# Using a Color object directly...\ncolor = Color(16, 200, 45)\n# ... which can also parse the CSS syntax\ncolor = Color.parse(\"#A8F\")\n
    "},{"location":"css_types/horizontal/","title":"<horizontal>","text":"

    The <horizontal> CSS type represents a position along the horizontal axis.

    "},{"location":"css_types/horizontal/#syntax","title":"Syntax","text":"

    The <horizontal> type can take any of the following values:

    Value Description center Aligns in the center of the horizontal axis. left (default) Aligns on the left of the horizontal axis. right Aligns on the right of the horizontal axis."},{"location":"css_types/horizontal/#examples","title":"Examples","text":""},{"location":"css_types/horizontal/#css","title":"CSS","text":"
    .container {\n    align-horizontal: right;\n}\n
    "},{"location":"css_types/horizontal/#python","title":"Python","text":"
    widget.styles.align_horizontal = \"right\"\n
    "},{"location":"css_types/integer/","title":"<integer>","text":"

    The <integer> CSS type represents an integer number.

    "},{"location":"css_types/integer/#syntax","title":"Syntax","text":"

    An <integer> is any valid integer number like -10 or 42.

    Note

    Some CSS rules may expect an <integer> within certain bounds. If that is the case, it will be noted in that rule.

    "},{"location":"css_types/integer/#examples","title":"Examples","text":""},{"location":"css_types/integer/#css","title":"CSS","text":"
    .classname {\n    offset: 10 -20\n}\n
    "},{"location":"css_types/integer/#python","title":"Python","text":"

    In Python, a rule that expects a CSS type <integer> will expect a value of the type int:

    widget.styles.offset = (10, -20)\n
    "},{"location":"css_types/keyline/","title":"<keyline>","text":"

    The <keyline> CSS type represents a line style used in the keyline rule.

    "},{"location":"css_types/keyline/#syntax","title":"Syntax","text":"Value Description none No line (disable keyline). thin A thin line. heavy A heavy (thicker) line. double A double line."},{"location":"css_types/keyline/#examples","title":"Examples","text":""},{"location":"css_types/keyline/#css","title":"CSS","text":"
    Vertical {\n    keyline: thin green;\n}\n
    "},{"location":"css_types/keyline/#python","title":"Python","text":"
    # A tuple of <keyline> and color\nwidget.styles.keyline = (\"thin\", \"green\")\n
    "},{"location":"css_types/name/","title":"<name>","text":"

    The <name> type represents a sequence of characters that identifies something.

    "},{"location":"css_types/name/#syntax","title":"Syntax","text":"

    A <name> is any non-empty sequence of characters:

    • starting with a letter a-z, A-Z, or underscore _; and
    • followed by zero or more letters a-zA-Z, digits 0-9, underscores _, and hiphens -.
    "},{"location":"css_types/name/#examples","title":"Examples","text":""},{"location":"css_types/name/#css","title":"CSS","text":"
    Screen {\n    layers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;\n}\n
    "},{"location":"css_types/name/#python","title":"Python","text":"
    widget.styles.layers = \"onlyLetters Letters-and-hiphens _lead-under letters-1-digit\"\n
    "},{"location":"css_types/number/","title":"<number>","text":"

    The <number> CSS type represents a real number, which can be an integer or a number with a decimal part (akin to a float in Python).

    "},{"location":"css_types/number/#syntax","title":"Syntax","text":"

    A <number> is an <integer>, optionally followed by the decimal point . and a decimal part composed of one or more digits.

    "},{"location":"css_types/number/#examples","title":"Examples","text":""},{"location":"css_types/number/#css","title":"CSS","text":"
    Grid {\n    grid-size: 3 6  /* Integers are numbers */\n}\n\n.translucid {\n    opacity: 0.5    /* Numbers can have a decimal part */\n}\n
    "},{"location":"css_types/number/#python","title":"Python","text":"

    In Python, a rule that expects a CSS type <number> will accept an int or a float:

    widget.styles.grid_size = (3, 6)  # Integers are numbers\nwidget.styles.opacity = 0.5       # Numbers can have a decimal part\n
    "},{"location":"css_types/overflow/","title":"<overflow>","text":"

    The <overflow> CSS type represents overflow modes.

    "},{"location":"css_types/overflow/#syntax","title":"Syntax","text":"

    The <overflow> type can take any of the following values:

    Value Description auto Determine overflow mode automatically. hidden Don't overflow. scroll Allow overflowing."},{"location":"css_types/overflow/#examples","title":"Examples","text":""},{"location":"css_types/overflow/#css","title":"CSS","text":"
    #container {\n    overflow-y: hidden;  /* Don't overflow */\n}\n
    "},{"location":"css_types/overflow/#python","title":"Python","text":"
    widget.styles.overflow_y = \"hidden\"  # Don't overflow\n
    "},{"location":"css_types/percentage/","title":"<percentage>","text":"

    The <percentage> CSS type represents a percentage value. It is often used to represent values that are relative to the parent's values.

    Warning

    Not to be confused with the <scalar> type.

    "},{"location":"css_types/percentage/#syntax","title":"Syntax","text":"

    A <percentage> is a <number> followed by the percent sign % (without spaces). Some rules may clamp the values between 0% and 100%.

    "},{"location":"css_types/percentage/#examples","title":"Examples","text":""},{"location":"css_types/percentage/#css","title":"CSS","text":"
    #footer {\n    /* Integer followed by % */\n    color: red 70%;\n\n    /* The number can be negative/decimal, although that may not make sense */\n    offset: -30% 12.5%;\n}\n
    "},{"location":"css_types/percentage/#python","title":"Python","text":"
    # Integer followed by %\nwidget.styles.color = \"red 70%\"\n\n# The number can be negative/decimal, although that may not make sense\nwidget.styles.offset = (\"-30%\", \"12.5%\")\n
    "},{"location":"css_types/scalar/","title":"<scalar>","text":"

    The <scalar> CSS type represents a length. It can be a <number> and a unit, or the special value auto. It is used to represent lengths, for example in the width and height rules.

    Warning

    Not to be confused with the <number> or <percentage> types.

    "},{"location":"css_types/scalar/#syntax","title":"Syntax","text":"

    A <scalar> can be any of the following:

    • a fixed number of cells (e.g., 10);
    • a fractional proportion relative to the sizes of the other widgets (e.g., 1fr);
    • a percentage relative to the container widget (e.g., 50%);
    • a percentage relative to the container width/height (e.g., 25w/75h);
    • a percentage relative to the viewport width/height (e.g., 25vw/75vh); or
    • the special value auto to compute the optimal size to fit without scrolling.

    A complete reference table and detailed explanations follow. You can skip to the examples.

    Unit symbol Unit Example Description \"\" Cell 10 Number of cells (rows or columns). \"fr\" Fraction 1fr Specifies the proportion of space the widget should occupy. \"%\" Percent 75% Length relative to the container widget. \"w\" Width 25w Percentage relative to the width of the container widget. \"h\" Height 75h Percentage relative to the height of the container widget. \"vw\" Viewport width 25vw Percentage relative to the viewport width. \"vh\" Viewport height 75vh Percentage relative to the viewport height. - Auto auto Tries to compute the optimal size to fit without scrolling."},{"location":"css_types/scalar/#cell","title":"Cell","text":"

    The number of cells is the only unit for a scalar that is absolute. This can be an integer or a float but floats are truncated to integers.

    If used to specify a horizontal length, it corresponds to the number of columns. For example, in width: 15, this sets the width of a widget to be equal to 15 cells, which translates to 15 columns.

    If used to specify a vertical length, it corresponds to the number of lines. For example, in height: 10, this sets the height of a widget to be equal to 10 cells, which translates to 10 lines.

    "},{"location":"css_types/scalar/#fraction","title":"Fraction","text":"

    The unit fraction is used to represent proportional sizes.

    For example, if two widgets are side by side and one has width: 1fr and the other has width: 3fr, the second one will be three times as wide as the first one.

    "},{"location":"css_types/scalar/#percent","title":"Percent","text":"

    The percent unit matches a <percentage> and is used to specify a total length relative to the space made available by the container widget.

    If used to specify a horizontal length, it will be relative to the width of the container. For example, width: 50% sets the width of a widget to 50% of the width of its container.

    If used to specify a vertical length, it will be relative to the height of the container. For example, height: 50% sets the height of a widget to 50% of the height of its container.

    "},{"location":"css_types/scalar/#width","title":"Width","text":"

    The width unit is similar to the percent unit, except it sets the percentage to be relative to the width of the container.

    For example, width: 25w sets the width of a widget to 25% of the width of its container and height: 25w sets the height of a widget to 25% of the width of its container. So, if the container has a width of 100 cells, the width and the height of the child widget will be of 25 cells.

    "},{"location":"css_types/scalar/#height","title":"Height","text":"

    The height unit is similar to the percent unit, except it sets the percentage to be relative to the height of the container.

    For example, height: 75h sets the height of a widget to 75% of the height of its container and width: 75h sets the width of a widget to 75% of the height of its container. So, if the container has a height of 100 cells, the width and the height of the child widget will be of 75 cells.

    "},{"location":"css_types/scalar/#viewport-width","title":"Viewport width","text":"

    This is the same as the width unit, except that it is relative to the width of the viewport instead of the width of the immediate container. The width of the viewport is the width of the terminal minus the widths of widgets that are docked left or right.

    For example, width: 25vw will try to set the width of a widget to be 25% of the viewport width, regardless of the widths of its containers.

    "},{"location":"css_types/scalar/#viewport-height","title":"Viewport height","text":"

    This is the same as the height unit, except that it is relative to the height of the viewport instead of the height of the immediate container. The height of the viewport is the height of the terminal minus the heights of widgets that are docked top or bottom.

    For example, height: 75vh will try to set the height of a widget to be 75% of the viewport height, regardless of the height of its containers.

    "},{"location":"css_types/scalar/#auto","title":"Auto","text":"

    This special value will try to calculate the optimal size to fit the contents of the widget without scrolling.

    For example, if its container is big enough, a label with width: auto will be just as wide as its text.

    "},{"location":"css_types/scalar/#examples","title":"Examples","text":""},{"location":"css_types/scalar/#css","title":"CSS","text":"
    Horizontal {\n    width: 60;     /* 60 cells */\n    height: 1fr;   /* proportional size of 1 */\n}\n
    "},{"location":"css_types/scalar/#python","title":"Python","text":"
    widget.styles.width = 16       # Cell unit can be specified with an int/float\nwidget.styles.height = \"1fr\"   # proportional size of 1\n
    "},{"location":"css_types/text_align/","title":"<text-align>","text":"

    The <text-align> CSS type represents alignments that can be applied to text.

    Warning

    Not to be confused with the text-align CSS rule that sets the alignment of text in a widget.

    "},{"location":"css_types/text_align/#syntax","title":"Syntax","text":"

    A <text-align> can be any of the following values:

    Value Alignment type center Center alignment. end Alias for right. justify Text is justified inside the widget. left Left alignment. right Right alignment. start Alias for left.

    Tip

    The meanings of start and end will likely change when RTL languages become supported by Textual.

    "},{"location":"css_types/text_align/#examples","title":"Examples","text":""},{"location":"css_types/text_align/#css","title":"CSS","text":"
    Label {\n    text-align: justify;\n}\n
    "},{"location":"css_types/text_align/#python","title":"Python","text":"
    widget.styles.text_align = \"justify\"\n
    "},{"location":"css_types/text_style/","title":"<text-style>","text":"

    The <text-style> CSS type represents styles that can be applied to text.

    Warning

    Not to be confused with the text-style CSS rule that sets the style of text in a widget.

    "},{"location":"css_types/text_style/#syntax","title":"Syntax","text":"

    A <text-style> can be the value none for plain text with no styling, or any space-separated combination of the following values:

    Value Description bold Bold text. italic Italic text. reverse Reverse video text (foreground and background colors reversed). strike Strikethrough text. underline Underline text."},{"location":"css_types/text_style/#examples","title":"Examples","text":""},{"location":"css_types/text_style/#css","title":"CSS","text":"
    #label1 {\n    /* You can specify any value by itself. */\n    rule: strike;\n}\n\n#label2 {\n    /* You can also combine multiple values. */\n    rule: strike bold italic reverse;\n}\n
    "},{"location":"css_types/text_style/#python","title":"Python","text":"
    # You can specify any value by itself\nwidget.styles.text_style = \"strike\"\n\n# You can also combine multiple values\nwidget.styles.text_style = \"strike bold italic reverse\n
    "},{"location":"css_types/vertical/","title":"<vertical>","text":"

    The <vertical> CSS type represents a position along the vertical axis.

    "},{"location":"css_types/vertical/#syntax","title":"Syntax","text":"

    The <vertical> type can take any of the following values:

    Value Description bottom Aligns at the bottom of the vertical axis. middle Aligns in the middle of the vertical axis. top (default) Aligns at the top of the vertical axis."},{"location":"css_types/vertical/#examples","title":"Examples","text":""},{"location":"css_types/vertical/#css","title":"CSS","text":"
    .container {\n    align-vertical: top;\n}\n
    "},{"location":"css_types/vertical/#python","title":"Python","text":"
    widget.styles.align_vertical = \"top\"\n
    "},{"location":"events/","title":"Events","text":"

    A reference to Textual events.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"events/blur/","title":"Blur","text":"

    The Blur event is sent to a widget when it loses focus.

    • Bubbles
    • Verbose
    "},{"location":"events/blur/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/blur/#code","title":"Code","text":""},{"location":"events/blur/#textual.events.Blur","title":"textual.events.Blur class","text":"

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"events/blur/#see-also","title":"See also","text":"
    • DescendantBlur
    • DescendantFocus
    • Focus
    "},{"location":"events/click/","title":"Click","text":"

    The Click event is sent to a widget when the user clicks a mouse button.

    • Bubbles
    • Verbose
    "},{"location":"events/click/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/click/#code","title":"Code","text":""},{"location":"events/click/#textual.events.Click","title":"textual.events.Click class","text":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/","title":"DescendantBlur","text":"

    The DescendantBlur event is sent to a widget when one of its children loses focus.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/descendant_blur/#code","title":"Code","text":""},{"location":"events/descendant_blur/#textual.events.DescendantBlur","title":"textual.events.DescendantBlur class","text":"

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.control","title":"control property","text":"
    control: Widget\n

    The widget that was blurred (alias of widget).

    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was blurred.

    "},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
    • Blur
    • DescendantFocus
    • Focus
    "},{"location":"events/descendant_focus/","title":"DescendantFocus","text":"

    The DescendantFocus event is sent to a widget when one of its descendants receives focus.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_focus/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/descendant_focus/#code","title":"Code","text":""},{"location":"events/descendant_focus/#textual.events.DescendantFocus","title":"textual.events.DescendantFocus class","text":"

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.control","title":"control property","text":"
    control: Widget\n

    The widget that was focused (alias of widget).

    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget: Widget\n

    The widget that was focused.

    "},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
    • Blur
    • DescendantBlur
    • Focus
    "},{"location":"events/enter/","title":"Enter","text":"

    The Enter event is sent to a widget when the mouse pointer first moves over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/enter/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/enter/#code","title":"Code","text":""},{"location":"events/enter/#textual.events.Enter","title":"textual.events.Enter class","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/","title":"Focus","text":"

    The Focus event is sent to a widget when it receives input focus.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/focus/#code","title":"Code","text":""},{"location":"events/focus/#textual.events.Focus","title":"textual.events.Focus class","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#see-also","title":"See also","text":"
    • Blur
    • DescendantBlur
    • DescendantFocus
    "},{"location":"events/hide/","title":"Hide","text":"

    The Hide event is sent to a widget when it is hidden from view.

    • Bubbles
    • Verbose
    "},{"location":"events/hide/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/hide/#code","title":"Code","text":""},{"location":"events/hide/#textual.events.Hide","title":"textual.events.Hide class","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

    "},{"location":"events/key/","title":"Key","text":"

    The Key event is sent to a widget when the user presses a key on the keyboard.

    • Bubbles
    • Verbose
    "},{"location":"events/key/#attributes","title":"Attributes","text":"attribute type purpose key str Name of the key that was pressed. char str or None The character that was pressed, or None it isn't printable."},{"location":"events/key/#code","title":"Code","text":""},{"location":"events/key/#textual.events.Key","title":"textual.events.Key class","text":"
    def __init__(self, key, character):\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description key str required

    The key that was pressed.

    character str | None required

    A printable character or None if it is not printable.

    Attributes Name Type Description aliases list[str]

    The aliases for the key, including the key itself.

    "},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable: bool\n

    Check if the key is printable (produces a unicode character).

    Returns Type Description bool

    True if the key is printable.

    "},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
    name: str\n

    Name of a key suitable for use as a Python identifier.

    "},{"location":"events/key/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
    name_aliases: list[str]\n

    The corresponding name for every alias in aliases list.

    "},{"location":"events/leave/","title":"Leave","text":"

    The Leave event is sent to a widget when the mouse pointer moves off a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/leave/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/leave/#code","title":"Code","text":""},{"location":"events/leave/#textual.events.Leave","title":"textual.events.Leave class","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/load/","title":"Load","text":"

    The Load event is sent to the app prior to switching the terminal to application mode.

    The load event is typically used to do any setup actions required by the app that don't change the display.

    • Bubbles
    • Verbose
    "},{"location":"events/load/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/load/#code","title":"Code","text":""},{"location":"events/load/#textual.events.Load","title":"textual.events.Load class","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/","title":"Mount","text":"

    The Mount event is sent to a widget and Application when it is first mounted.

    The mount event is typically used to set the initial state of a widget or to add new children widgets.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/mount/#code","title":"Code","text":""},{"location":"events/mount/#textual.events.Mount","title":"textual.events.Mount class","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

    The MouseCapture event is sent to a widget when it is capturing mouse events from outside of its borders on the screen.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_capture/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was captured"},{"location":"events/mouse_capture/#code","title":"Code","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture","title":"textual.events.MouseCapture class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Sent when the mouse has been captured.

    • Bubbles
    • Verbose

    When a mouse has been captured, all further mouse events will be sent to the capturing widget.

    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when captured.

    "},{"location":"events/mouse_down/","title":"MouseDown","text":"

    The MouseDown event is sent to a widget when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_down/#code","title":"Code","text":""},{"location":"events/mouse_down/#textual.events.MouseDown","title":"textual.events.MouseDown class","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_move/","title":"MouseMove","text":"

    The MouseMove event is sent to a widget when the mouse pointer is moved over a widget.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_move/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_move/#code","title":"Code","text":""},{"location":"events/mouse_move/#textual.events.MouseMove","title":"textual.events.MouseMove class","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_release/","title":"MouseRelease","text":"

    The MouseRelease event is sent to a widget when it is no longer receiving mouse events outside of its borders.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_release/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was released"},{"location":"events/mouse_release/#code","title":"Code","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease","title":"textual.events.MouseRelease class","text":"
    def __init__(self, mouse_position):\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description mouse_position Offset required

    The position of the mouse when released.

    "},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

    The MouseScrollDown event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved down.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_down/#code","title":"Code","text":""},{"location":"events/mouse_scroll_down/#textual.events.MouseScrollDown","title":"textual.events.MouseScrollDown class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

    The MouseScrollUp event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved up.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_scroll_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_up/#code","title":"Code","text":""},{"location":"events/mouse_scroll_up/#textual.events.MouseScrollUp","title":"textual.events.MouseScrollUp class","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_up/","title":"MouseUp","text":"

    The MouseUp event is sent to a widget when the user releases a mouse button.

    • Bubbles
    • Verbose
    "},{"location":"events/mouse_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_up/#code","title":"Code","text":""},{"location":"events/mouse_up/#textual.events.MouseUp","title":"textual.events.MouseUp class","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"events/paste/","title":"Paste","text":"

    The Paste event is sent to a widget when the user pastes text.

    • Bubbles
    • Verbose
    "},{"location":"events/paste/#attributes","title":"Attributes","text":"attribute type purpose text str The text that was pasted"},{"location":"events/paste/#code","title":"Code","text":""},{"location":"events/paste/#textual.events.Paste","title":"textual.events.Paste class","text":"
    def __init__(self, text):\n

    Bases: Event

    Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description text str required

    The text that has been pasted.

    "},{"location":"events/resize/","title":"Resize","text":"

    The Resize event is sent to a widget when its size changes and when it is first made visible.

    • Bubbles
    • Verbose
    "},{"location":"events/resize/#attributes","title":"Attributes","text":"attribute type purpose size Size The new size of the Widget virtual_size Size The virtual size (scrollable area) of the Widget container_size Size The size of the container (parent widget)"},{"location":"events/resize/#code","title":"Code","text":""},{"location":"events/resize/#textual.events.Resize","title":"textual.events.Resize class","text":"
    def __init__(self, size, virtual_size, container_size=None):\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose
    Parameters Parameter Default Description size Size required

    The new size of the Widget.

    virtual_size Size required

    The virtual size (scrollable size) of the Widget.

    container_size Size | None None

    The size of the Widget's container widget.

    "},{"location":"events/screen_resume/","title":"ScreenResume","text":"

    The ScreenResume event is sent to a Screen when it becomes current.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_resume/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/screen_resume/#code","title":"Code","text":""},{"location":"events/screen_resume/#textual.events.ScreenResume","title":"textual.events.ScreenResume class","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

    The ScreenSuspend event is sent to a Screen when it is replaced by another screen.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/#attributes","title":"Attributes","text":"

    No other attributes

    "},{"location":"events/screen_suspend/#code","title":"Code","text":""},{"location":"events/screen_suspend/#textual.events.ScreenSuspend","title":"textual.events.ScreenSuspend class","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"events/show/","title":"Show","text":"

    The Show event is sent to a widget when it becomes visible.

    • Bubbles
    • Verbose
    "},{"location":"events/show/#attributes","title":"Attributes","text":"

    No additional attributes

    "},{"location":"events/show/#code","title":"Code","text":""},{"location":"events/show/#textual.events.Show","title":"textual.events.Show class","text":"

    Bases: Event

    Sent when a widget has become visible.

    • Bubbles
    • Verbose
    "},{"location":"examples/styles/","title":"Index","text":"

    These are the examples from the documentation, used to generate screenshots.

    You can run them with the textual CLI.

    For example:

    textual run text_style.py\n
    "},{"location":"guide/","title":"Guide","text":"

    Welcome to the Textual Guide! An in-depth reference on how to build apps with Textual.

    "},{"location":"guide/#example-code","title":"Example code","text":"

    Most of the code in this guide is fully working\u2014you could cut and paste it if you wanted to.

    Although it is probably easier to check out the Textual repository and navigate to the docs/examples/guide directory and run the examples from there.

    "},{"location":"guide/CSS/","title":"Textual CSS","text":"

    Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed.

    VSCode User?

    The official Textual CSS extension adds syntax highlighting for both external files and inline CSS.

    "},{"location":"guide/CSS/#stylesheets","title":"Stylesheets","text":"

    CSS stands for Cascading Stylesheet. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies styles to widgets, but otherwise it is the same idea.

    Let's look at some Textual CSS.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    This is an example of a CSS rule set. There may be many such sections in any given CSS file.

    Let's break this CSS code down a bit.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    The first line is a selector which tells Textual which widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class Header.

    Header {\n  dock: top;\n  height: 3;\n  content-align: center middle;\n  background: blue;\n  color: white;\n}\n

    The lines inside the curly braces contains CSS rules, which consist of a rule name and rule value separated by a colon and ending in a semicolon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semicolons.

    The first rule in the above example reads \"dock: top;\". The rule name is dock which tells Textual to place the widget on an edge of the screen. The text after the colon is top which tells Textual to dock to the top of the screen. Other valid values for dock are \"right\", \"bottom\", or \"left\"; but \"top\" is most appropriate for a header.

    "},{"location":"guide/CSS/#the-dom","title":"The DOM","text":"

    The DOM, or Document Object Model, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure.

    Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These child widgets form the branches of the tree.

    Let's look at a trivial Textual app.

    dom1.pyOutput
    from textual.app import App\n\n\nclass ExampleApp(App):\n    pass\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    ExampleApp

    This example creates an instance of ExampleApp, which will implicitly create a Screen object. In DOM terms, the Screen is a child of ExampleApp.

    With the above example, the DOM will look like the following:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()

    This doesn't look much like a tree yet. Let's add a header and a footer to this application, which will create more branches of the tree:

    dom2.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\n\n\nclass ExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    ExampleApp \u2b58ExampleApp

    With a header and a footer widget the DOM looks like this:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1aa0/bSFx1MDAxNP3Or0DZL7tS4877UWm1glx1MDAxNpZ3aWFcdTAwMDNlVVWuPSReP2s7QKj633dsgu28XHUwMDFjXHUwMDEzXHUwMDEyNpXWQiGZmdy5nrnnzLk3/r6xudlKXHUwMDA3kWq92WypO8v0XHUwMDFjOzZvW6+y9lx1MDAxYlx1MDAxNSdOXHUwMDE46C6Uf07CfmzlI3tpXHUwMDFhJW9ev/bN2FVp5JmWMm6cpG96Sdq3ndCwQv+1kyo/+SN7PTF99XtcdTAwMTT6dlx1MDAxYVx1MDAxYuUkbWU7aVx1MDAxOD/MpTzlqyBNtPW/9efNze/5a8U72zH9MLDz4XlHxT0hx1tPwiB3XHUwMDE1Ulx1MDAwNLEkklx1MDAxNFx1MDAwM5zknZ4sVbbuvdZcdTAwMGWrsidralxyTi//7Jn/qItLwY5Odq7hh17nvJz12vG8s3TgPayDafX6sSp7kzRcdTAwMGVddeHYaS+bfKy9+F5cdTAwMTLqJSi/XHUwMDE1h/1uL1BJMvKdMDItJ1x1MDAxZOg2XG6KRjPo5ibKlrtsXHUwMDAxIDVcdTAwMTBcdTAwMTdYMIwpXHUwMDA2RJS3m31fSINRhFx05nTMo7ehp3dAe/RcdTAwMGLIr9Knr6bldrVjgV2OXHUwMDExyJKwcre3j/dZma+nnG4vzVx1MDAxYVx1MDAxMTJcdTAwMDQgTHD6YLuyXHUwMDFhKl99yCDjknAhip5sxmjfzsPg8/jq9cw4XHUwMDFhrlIryT5UvM1cdTAwMWPdXHUwMDE5j6FqXHUwMDFjVXbY/bgtrlx1MDAwZTBoR9euc9x7f+6eqm+FrZGgS9Vd2io6fryqM1x1MDAwYjto76vf+3JLOsHX+Pro+vh+f3u6WTOOw9umdpfu7sjoV00nLM1cdTAwMGXflfvTj2wzXHUwMDFkbilcdTAwMDNcdTAwMDTpXHUwMDAwpITgot9zXHUwMDAyV3dcdTAwMDZ9zyvbQsstMbhR8XdcdTAwMDL5I35WYVx1MDAwZsFM2DMuJEKYoca4r1/mNcU9XHUwMDAydbiHXHUwMDA0XHUwMDFhlOZcdTAwMTB8XHUwMDBl7tPYXGaSyIw1tqZgn0/Dvlx1MDAxY8c6oYBwKjFaPtSXXHUwMDE5h+V2h0F65tw/xJJBNcNcdTAwMDHEXHUwMDAw4lJcdTAwMDLKR0btmr7jXHJGdjBcdTAwMGZY7fnOnelHntqKol9/q65worQnuWky8p0tz+lmgd2y9L2peCTmU0efm8VcdTAwMDDfsW2vXHUwMDEyf5Z2xNQ24/0mZ1hcdTAwMTg7XScwvfNpftZCMVZW+lx1MDAxMIpT8EhcdTAwMTmcjcd87Vx1MDAwNGON8eh9ROdcdTAwMDcn51dXXHUwMDFjfGA+3btcbkkvXXc8YmhcdTAwMDDGXHRcdTAwMTFiXHUwMDFhXHUwMDFlXHUwMDExpVx1MDAwNuBcdTAwMDTBlVx1MDAwMpLSSUBcbq55YkxcdTAwMDA8XHUwMDFlwlx1MDAxNHLOKV9cdTAwMDEy6061I6ftfzlcdTAwMWFcdTAwMWO+9c47g0/3XHUwMDFkeb69dbK+h/DFQefm9oi8Oz6Kqf3nXHUwMDAw9Vx1MDAxMXnHlmBcdTAwMTdd2Pt7u651LLZcYjz3vfc7wVV3XHR2l76880TD9Fx0XHUwMDFiekturtt+6rZcdTAwMDdcdTAwMWbIXHUwMDE3c0dYd+q47y9hXHUwMDE1tlx1MDAwZb+ddtPww5dTR4pcdTAwMDO307uEnzrN7DZcdTAwMTE5XHUwMDE4gVx1MDAxMjUrXHUwMDEyOYTNXHUwMDE2OZhcbkkolOWIeaRaXHUwMDFmXHUwMDE260qqrJZUXHUwMDA1MzhcdTAwMDTymclNPaeSKZyKSmHxyKVQICQpYCtIaJZcdTAwMTmI01RcdTAwMGVcdTAwMDIjrTWq5syKlVxuZilcdTAwMWE+Mn5pimaOXHUwMDFhXHUwMDE4VzSFj7WYe8D8XHUwMDE00HExXHUwMDEzc1x1MDAxMFx1MDAxMEq0lm1eUKg/ktZcdTAwMTNzXHUwMDE4XGJDUlx1MDAwMTiejjnIXHImZSZkiMyulSFcdTAwMGZcdTAwMThEslFwXHUwMDE3XHUwMDAwxMSQXHUwMDFjMcrQpKrRniGh4bhcdTAwMDBcdTAwMTJz71x1MDAxNkWiwIwvgsQkNeN021x0bCfo6s7yLNNotPrZvG1gXHUwMDAwrNVcdTAwMWGRmlxmqURcdTAwMDSI4raz2zOjbGNcckKylIdSJlx0YkRWRlxmS2x1XHUwMDE5wnBwcai2VGDPdVxuSE2/gOtcZkn/UV5ip/Bcblx1MDAxOTp7ytVnXlx1MDAwN4KYz3JrOswn3PLMJH1cdTAwMWL6vpPq5T9ccp0gXHUwMDFkX+Z8PbcyfPeUaY/36tuq9o1cdTAwMTNBlFlcdTAwMWNVsOW7zVx1MDAxMir5h+L951dTR7cnQzi7KsFbWtio/l8oXHUwMDA3g1x1MDAwMM1OwpD2g2KEmlx1MDAxN0VOXHUwMDBmXHUwMDA3b4OrvnQv/Y8n9uG9+5f7z81/y1061uaQXHUwMDE31OSFMIJcdTAwMDRcdMr1a4XNM1x1MDAwM4RcdTAwMTBDY4Q9dldy0v84XHUwMDE304QlKIa0dOhFUjHn6F03OtlNXHUwMDBmLv0t92T70N/12zPU9/+p2NPtrmh5l252XoY3fcKG3j4jw3skxdnHblx1MDAwNqfKT0ArysQkJuOtXHUwMDE1ZoX64GWoeXmrfvvWlVkhrmVWzlxmJiBmkIFVM2uzjFxmccZYRqsvmpA9OVx1MDAxZZ+XkO1pXHUwMDE1o+JcdTAwMTdOyOYog/GErPDxXHUwMDE50oaBurSMIy2NafNSiJfsXHUwMDFlmGfx9o06oJ3j+z3w7XhcdTAwMWKsO1x1MDAwMDFhXHUwMDA2gZzAXHUwMDFjX9mvXHUwMDFiY9KGXHUwMDFiWId8MWBdlI3kOkmQ1VrWiyib40+nZF/sWIdcdTAwMDNcdTAwMTd+ci/aN1F/XHUwMDAw/1c2y1I2K1ren8XsPME0fcKG3q60dI04rXLNilx1MDAwNFx1MDAxM6SYjTdcdTAwMTeEzTHQvoAnKKb6/VtXwqawlrC5NCjEWFx1MDAwZVx1MDAwNdVcblx0u2FcdDvLPzlcdTAwMDGlmy8jmJ5cdTAwMTiPz1x1MDAxM0y7YZi+uGCaozfGXHUwMDA1U+FjLfRmVrBcdTAwMTma/UicoEBcdTAwMTJcZnDzXHUwMDEydn32tq7QXHUwMDAzwFx1MDAxMFRcYplVQ1x1MDAwNVx1MDAxNmhcdTAwMDR5WEslwXV+wIfIw6uDXHUwMDFlXHUwMDAyhqSMS0klg1JCMYlEgVxyqZNIJFx1MDAxONY+M4nGgUmAhFx1MDAxNEm0XHUwMDAwMJ9R0F48k5ld0G5Q8C2PuWqlmVBcdTAwMDBcdTAwMDGlXHUwMDAy6pVgXGLDyqjH6jdFXHUwMDE0wmH2KTBcdTAwMWVcdTAwMGWYX89cdTAwMWXxqT61XHUwMDE59YkhoONcZnFJXHUwMDAw1uGEJnyCyOA6USZMZ8Ukq1x1MDAxM6BcdKd+qmr27GDOrokwLu1tVP8/mc8gwLNcdI3RXGbmXFw011x1MDAxMvXqal1cdFxyXHUwMDBiXHUwMDAzScFcdTAwMDVikmnJUFx1MDAxZVRcdTAwMGaEpqVcdTAwMDTCNPuVnFxiQenqXGJNYoNBiHQ8M4xw5cHeks6kgVx1MDAwNOeSUo6ZoHLy2V/B9Z2QhTLCZ/HZokJj2XxcdTAwMDZcZk1j2lx1MDAxYlx1MDAwMfR2MSRk+ZxiwVx1MDAxZNyA+TOTXHUwMDEwPGzognxWrzxGfKJASL1OTGtcdTAwMDRcbinhXHUwMDEzLlx0gzJ9XGZcdTAwMDGYXHUwMDFmm1iIn5rNZlx1MDAwNXJ2TYTwLCrbXHUwMDE4mm+ZUXSW6ngrtkKHtGNcdTAwMGbVaXmPrVx1MDAxYkfdbk95vP46vzLBl69mxkIqu9PvPzZ+/Fx1MDAwYlx0sVx1MDAwYuIifQ== ExampleApp()Screen()Header()Footer()

    Note

    We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.

    Both Header and Footer are children of the Screen object.

    To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:

    • textual.layout.Container For our top-level dialog.
    • textual.layout.Horizontal To arrange widgets left to right.
    • textual.widgets.Static For simple content.
    • textual.widgets.Button For a clickable button.
    dom3.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\n\nQUESTION = \"Do you want to learn about Textual CSS?\"\n\n\nclass ExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n        yield Container(\n            Static(QUESTION, classes=\"question\"),\n            Horizontal(\n                Button(\"Yes\", variant=\"success\"),\n                Button(\"No\", variant=\"error\"),\n                classes=\"buttons\",\n            ),\n            id=\"dialog\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.

    Here's the DOM created by the above code:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1d+1PiyFx1MDAxNv59/oop95e9VWO2+/T7Vt265Vx1MDAwYnVcdTAwMWSfOFx1MDAwYnrnllx1MDAxNSFcdTAwMDIjLyGIurX/+z1cdTAwMWSVXHUwMDA0Qlx1MDAwMihB2DvU7qhJ6Jx0n/Od7+tX/vz0+fOa/9j21v75ec17KLn1Wrnj9te+2OP3XqdbazXxXHUwMDE0XHUwMDA0f3dbvU4puLLq++3uP3/7reF2bj2/XXdLnnNf6/bcetfvlWstp9Rq/FbzvUb33/bfI7fh/avdapT9jlx1MDAxM95k3SvX/Fbn+V5e3Wt4Tb+Lpf9cdTAwMDf//vz5z+DfiHXlmttoNcvB5cGJiHlGjlx1MDAxZT1qNVx1MDAwM1NccuGCXHUwMDAzIzC4oNbdxpv5Xlx1MDAxOc/eoMFeeMZcdTAwMWVa29y+K3b2XHUwMDFlL5t5wXP7f0C+d212w7ve1Or1vP9Yf65cdTAwMDe3VO11vPBs1++0br1CrexX8TxcdTAwMWQ5Pvhet4VVXHUwMDEwfqvT6lWqTa/bXHUwMDFk+k6r7ZZq/iNcdTAwMWVcdTAwMTNkcNBtVoJcIsIjXHUwMDBmwVx1MDAwNeBQJkBcdTAwMTCquDBE8cHp4PvUYYRqo1x1MDAxOFBcdTAwMGVajtq11apjO6Bdv5DgXHUwMDEzWnbtlm4raF6zXHUwMDFjXqOhZGjkmfuvT6uZwzlcdTAwMDPA21x1MDAxOGZcYojBJVWvVqn69lx1MDAxYVx1MDAwMEdcdTAwMTMutVx1MDAxMs+3ipjiXHUwMDA1TUJcdTAwMDVQ4FLqsEmtXHUwMDAx7f1y4Fx1MDAxYv9cdTAwMWSt0qrbab9U3VrX/lx1MDAxMTHe2r0z6lhR54o0u5bnXHUwMDFi5sdlsVrq7JOTk8vCuVuQg7KGPNH3XHUwMDFl/LXBib++pFx1MDAxNXt4uv90kXso+p1tb72a71x1MDAxNlx1MDAxNfCb8cW6nU6rP225XHUwMDE5mTv3Yoeu/jLtXHLDYl9+XHUwMDBim73XLrvPwUulJFxco1trqfXgfL3WvMWTzV69XHUwMDFlXHUwMDFla5Vuw3j/XHUwMDE0sTeGMkN2RiGG6NGjr1x1MDAxMCOAXHUwMDE50IqEXHUwMDBlO1x0YtJreVkhRqVBXGZFXHUwMDA0XHUwMDAywoFTot6PMX7HbXbbblx1MDAwN1x1MDAwM3dcZs6oyThcdTAwMDNxXFxB41xmXHUwMDEwakK754Yr8/TO0Fx1MDAwYlpNP1978oKyXHUwMDFjQTUnIFx0KGOIUENX5dxGrf441LCBXHUwMDFio+VcdTAwMWLt9q//iFZ110NcdTAwMTOCMsXQxVx1MDAxYvVaxfr5Wlx0XHUwMDFmyutcZoWAX8OUPbigUSuX61x1MDAxMX8soVx1MDAwNS6W2dmfJn22OrVKrenWz4dcZkxccsmOV/KffXJMXFxcbkWS4pIqIyhorvTUgVx0rdO9XHUwMDFi/fitcX2+sd+oXHUwMDFlVcVV/2TZXHUwMDAzkyrHXHUwMDAwZ1x1MDAxNORzYFx1MDAwZcUloO8oYFxu/3/O/dmFpVx1MDAxOFx1MDAxM4daOVx1MDAxOKacKVx1MDAxMYtHrlxyIZpcbj3/eEzLcGfyxtvtq1x1MDAxZtWzwl3ha+OSi527+vLm+cLvf9z3v/Ltw69cdTAwMWRR3n2EXHUwMDFl8O2EhDxTuVAo7+/lbkuHeoPT80b9eKd5WZlDuVx1MDAxOVVvRsWq+9pe7m6HNjo7V+Vt/1x1MDAxZe6P6/OoXHUwMDA1Tjpt2rwvXHUwMDFkXHUwMDE1clx1MDAwN0e6sF1oXHUwMDFk+lx1MDAwN+8qd1x1MDAxMo9cdTAwMWFfQVOae/2jsXFSv5dU+OubZVM5OGo9PE5n7rLwMy7V6NFBXHUwMDFlkIgykis2fVx1MDAxZUh3tyXNXHUwMDAzKK1S8lx1MDAwMNdcdTAwMGXD9jCv/Cy7PMDH8TFcdTAwMTbDf6VBWDKTgc7Lmo9cdTAwMDFcdTAwMTk6msK/8qWO5zWTKJhcdTAwMWG6fm5cdTAwMTRsXHUwMDAyi1x1MDAxOaVgXHUwMDAzXHUwMDFiU1x1MDAwM+858MdEnoakwFx1MDAwM+CCXHUwMDEzXHUwMDFkYdyT4i49i35I3FEyMfCMcDTlhqlxgYcs1GFcXFx1MDAwZlximMgs8IjDjSQmyrVcdTAwMDbxx7hjXHUwMDE0SCEhTsRcdTAwMDQzmnEuYfZAXGasW3Qgdn2342/WmuVas4Inw3yGwVjq2fuuXHUwMDEzhzAqXHUwMDA1R3RRXHUwMDAyQZGE1W5cdTAwMWbPbdtmQ1x1MDAxMSlQR1xuIVxyXHUwMDA3yU3kipfeyDRF83LxILGuec3yRKOIUehPXG6lXHUwMDFj/idUqExcdTAwMDZWgYMyLyDMQe9cdTAwMThlKsms8VFcdTAwMWUzq+52/a1Wo1HzsfpPWrWmP1rNQX1u2PCuem559Cw+VvTcKFx1MDAwZbRticOkO/ztc1x1MDAxOCnBXHUwMDFmg9//+2Xs1etxXHUwMDE3tp+I84YlfIr+fJN0pJSz0cOv0MUlw3yKXjo1dDVcdTAwMGVcbnDyXGJ721vffpQ2L2BcdTAwMDOKXHUwMDA172Ohi09CLkaZI1x1MDAxOJJcdTAwMDFuOOJTRIpcdTAwMDVfZ9rRRFx1MDAxM2OoXHUwMDA0TcjSSEfJXHUwMDA1YGTDgnuIycPW9UFLXlx1MDAxZJT13f3ZUfuGnlx1MDAxZPKfynFeyjGj6l2tYucvSCdcdMfxXHUwMDBmXHUwMDEyXHUwMDE2+4qzXHUwMDFmLfAoiYDEXGJaY1wipVxiUtN3wKe33rKCtU5cdTAwMDNrRVx1MDAxY8OxNZShXHUwMDAxWC+BwFx1MDAwM6Q7iohcdTAwMGbocDczeOP7XHUwMDA03lx1MDAxZdJcIq+zYIE3gWuMXG68gY3v4Epa0KTo41x1MDAxMt2NSja9zDvYOqnkjqhqyZvzvavTi2qp+vWDx78mhlx1MDAxZlx1MDAxMlBkQ1x1MDAwNFx1MDAwNZ4kmqlcYvlcYr7OiaMkRiVjWiFtJ9nJvFx1MDAxOclcdTAwMTJcYoPijrxB3L2HK12RnD686lx1MDAxNVx1MDAwZp425FaDbPbORK/4kyvNiytlVL0/i/2QvvvxXHUwMDBmMjNcdTAwMDWbJem9jYIpZUZcdTAwMGZcdTAwMGYmQVx1MDAxMKlcdTAwMTjRfHpcdTAwMGWW3nxLmlx1MDAwNFx1MDAxOE1LXHUwMDAyXG5cdTAwMWNtXGZRQFnWSWA6XHUwMDBlXHUwMDA2XHUwMDA0oVx1MDAxZqniR3SyL46D5Votf+FcdTAwMWNsXHUwMDAyh1x1MDAxOeVgXHUwMDAzXHUwMDFiUyMvuZNd8sTIk8LYvs7pIy9dZC7p6JamXHUwMDBlhlx1MDAxNuGGcEKJNEORx4hxJDOCaFx1MDAwM1ooynl2kWdQaFx1MDAxOabBSKBcXI2LQ2rn63BGXHUwMDE4wlx1MDAwMGgqqVx1MDAxOY1LKvCo5lGVtrA+9zfFZXKf+1x1MDAxNH3SYcqLdoZzQTUxQiCY2s68cETyc9hDLzmAeVx1MDAxZLJk7OWCyX3uQ0alq6Vho7CtXHUwMDEwuLlcdTAwMTRcdTAwMWFcdTAwMTRmu7hNXHUwMDE0XHUwMDFjXHRCXGKtJeVcdTAwMTRcZo/ZtFJcdTAwMWTuid5sP3E/XHUwMDBly/tcdTAwMTT9OTOcof8ndr1TJrF6XHKfYT5lOmVbTkCTTDvAqMK4tLOhR1x1MDAwMY1Sh1wi2+DUgLQ/Mlx1MDAwNDTJXHUwMDFkaWPMoFSkhof9SpFZ29QhdpYrXHUwMDA1jf9Hc81gNpfSXGZcdTAwMTBkVlx1MDAxZtBe0YA41FBlQYrhXHUwMDAzXHUwMDAzeiRcdTAwMGKD41x1MDAwNVxyXHUwMDE0Nlx1MDAxMuproGiMXHUwMDE2KP4jV7xlsG7SXHUwMDE4XCJxbIojXHUwMDFjMHtcYkFcdTAwMDVXMZO0g4RcdTAwMDBcdTAwMTDFlE2ETOskk8ZcdTAwMTOYlYazRFdcdTAwMGVOxpx4XmiGzZ9cdTAwMDRmKFx1MDAwNdArpJl+XHUwMDFjMX0u1pJOgUA1ZFeYIEkzXFxhK1xmz1xyt8OMhklCMckwxSC7rmnpXHUwMDE4XHUwMDE0XtyuMEF6xWSYZFwi5Fxmg1xiXHUwMDA1kUJYZaBRxsWwjGDcc6pM+IyrimVvJmdAQFAk2kZgXHUwMDE2UiQ+e8Jgi2rkb1x1MDAxYUOKMKNeqcGM3Kx0ly/+vv/Acr9fX5y6hzfXh+R4N8EmgsmQXCJhMVx1MDAwMVx1MDAxZFHAY0ZR7mhO0dHAXHUwMDAwIKc0sNJwtp7ozvZcdTAwMTN35Fx1MDAxOfEstcNcdTAwMWaISOzroUhJXHUwMDE4g1kmdqW385KiXHUwMDFh5n5cdTAwMDdcdTAwMTBcbjhcdTAwMTOgsFx1MDAxMUKkeJ5cdTAwMWVhXHUwMDFjhjrBKNQoQI1cdTAwMWGxa55cZi1EzLDHP0wpr7jFgGk0KFSkXHUwMDBi6ep/8vvC2z2R3fLvKt8/qNxvbVx1MDAxZW/+7OqfV1d/RtW7WsVmNU9/tWrhXHUwMDFk0/RcdTAwMTPKnTQyMf5BpjT3x7fqU1x1MDAxOfLFYt4tnpeqV4WN+lnCmo2ZXHUwMDFh7eBcXO+c9HKHXHUwMDA3Z113vXrQudl4qO9OV+5rXpwjXHUwMDBiS02xyatJVeJyXHUwMDA14NoopF7TS4Z0d1vW5FxuLC25XG6kcotJrmJMco0ssHxNrlx1MDAxMrOrnbKbQXbNeiSFyqGjKSMpW69jXHUwMDFjv35v2lx1MDAxM7Xyv77bjVx1MDAxN+qtyve1783xIyxcdTAwMDKGylx1MDAxOVxmoNS9m2Hnn2l8ZVx1MDAwMmNcdTAwMWNcdTAwMWRfmWj5O6gwJ4mDL+jAXHUwMDAwis6wvcTR3vXm7Teyv5477uzU1pv89NvjzrJcdTAwMDer5Kg17LwzrpRcclZcdTAwMThcblZhuGNQh2CsZlx1MDAxZKxcdTAwMTFNXHUwMDFlMuF4b6TQilx0XHUwMDEzXHUwMDFkgF1cYlx1MDAxNW7vNftcdTAwMTe3x9c7ZVLNre+vXHUwMDFmk3rx8CdcdTAwMTWeXHUwMDE3XHUwMDE1zqh6V6vYrKjwatXC/KlwRuZOYtjjb5g9XHUwMDEzTi13v3/WKPTFjr7eJFx1MDAxN/6RPKrRq4TJ7TOVu9voXHUwMDFll76dNd2LgtgvbNx5lav1/nTlLlxyc+cscVwirFCEK1x1MDAxMd1cdTAwMTllXHUwMDEyXHUwMDE5SHe3pSVcdTAwMDNcIoVcZkhiXHUwMDFjvlx1MDAxODKgx5CBMczdjklcdTAwMTi7XHUwMDA2/O/M3PeQXHUwMDEwP1lcdTAwMGVcXP/VXHUwMDFlfybBpbrb7XpdZMLXPd9vNbuLJvFcdTAwMTPIbmyi+lxmXHUwMDBm8Vx1MDAwZT4vk/eM4VRwXCJcdTAwMDUjU4dweX/f39l+2uHXrFuu7l+oRv/SXfZcdTAwMTBcdTAwMTZ2N1x1MDAwMCUkl1JcdTAwMDCG8PB4nZLS4YBS1273xIBnuJXTlHzeLmoxoFx1MDAxNzyJfbt597he8lxi9b8+Ulwi+24+d978SefnReczqt7VKjYrOr9atZBcdTAwMTWdX61aqJvNLX29u8W3pPTy51x1MDAwN1x1MDAwZpVcXG5KevxG9TH+Qf6P2Hx0ksdo11x1MDAxZWFMaVxyYno6n+5cdTAwMTfLylx1MDAwNVx1MDAwNEvjXHUwMDAyii6KXHUwMDBijKPzKsZcdTAwMDVcZppIuFAruKp0eja/XHUwMDE5MN1cYlx0/r524SHx/fL8173bqblNXHUwMDFmKXG3Vyrh0yXzejVcXPiceP1cdTAwMDTSO8rr3/Y472D4KD6TwtpQ6+UgZpiR97Tlnp6ap7PKZvGylds4h0b+dNmjWlx1MDAxOe1IXHUwMDAzxE5cdTAwMDWFmEhXUjh2wi5+1FIwfCCUMY46fcG7QbKvxW5+s974arbyxe5cdTAwMWb8x5Ms/tzTY25cdTAwMTQ/o+pdrWKzovirVVx1MDAwYllR/NWqhflT/IzMnaRcdTAwMWPG33BKa98xvvDy28crh2h/cmxcdTAwMDVcdTAwMTNcdTAwMDXgTNLplUN6+y0px9CEpXFcZkVcdTAwMTbFMaZTXHUwMDBlVFx1MDAxOY5qLjqz6v9DOlx1MDAxY7XGUG1cdTAwMGZcdTAwMDOrs2jdMIFKT6FcdTAwMWImPUtqMKeLXHUwMDA2XHUwMDAxyX1cdTAwMDFcdTAwMDJQz3NcdTAwMDPTr0ms9o+L9fouK/hf20y15GX+zr1e9ohmmjtcdTAwMTiqWmKYXHUwMDA0fVx1MDAwMWIopIWRjkBtpbnKXFw2jFm6M2ZgwFx1MDAxMIFBRt+yd+l7ZMN957Kfq2z+qKpK+3ZrY+Obq9tnP2XDvGRDRtW7WsVmJVx1MDAxYlarXHUwMDE2spJccqtSXHUwMDBik3j4+Fx1MDAxYi4hX1x1MDAxNjqRL2thqN1cdTAwMGV++uSaXs1Lm1xcTVpylYQuKrnqMcl1XGZfJswgX1ZvWFx1MDAwNrs6dDnvu8hgXyaNn37byZ/vXHUwMDFmXHUwMDFmfbF/jM4+uet5Xb+WOokmXHUwMDFi0jyBSca283/rXHUwMDEzpcZ14uJ3mVx1MDAxONacXHUwMDFhLYDNsJAlfcXQkoZ1sP0/U6hcdTAwMTCQjGL0XHUwMDBl78sqXHUwMDE4c4BJxlXmKlx1MDAxOIyDXHUwMDE1zijeXGLsmmFcdTAwMTVcdTAwMGZyyZ3xu0RyorRcdTAwMWHaROpvseR9luXlQjBit8IhWIfcaFx1MDAxYbnqdTtcIq00XHUwMDExxK7GJnY19stcdTAwMDVcdEveh1x1MDAxZmOVlp0ne5L9XGZ8KCznU/Tn7JtcdTAwMDFFXHUwMDEy/ygz0NpcbrxcdTAwMTmW16RPiV5SXGKRXHUwMDAwXHUwMDBlXHUwMDE3WNtCWM+iw3tcdTAwMDFcdCZcdTAwMWNcdEoxXHUwMDA1zDBcdTAwMDZmxK45QlxikY7dt9A6ONGM6DFcdTAwMTBcIrD57Yt77IxcdTAwMDBjKJD4XHUwMDBiRYDYxXJcdTAwMWM+4IVcIktcdTAwMDEmxOFKcFCSSqVcdTAwMTFRTPw1XHUwMDFm2lx1MDAwMWk4J4Tad3wgXHUwMDE3TMeSJJPS59eOmKSEXCJUMlwiqNAmvpdcdTAwMTF37I5mTHGBXHUwMDE2XHUwMDBiyVTMpFVcdTAwMDKxZFe2n7hcdTAwMTPPXHUwMDBizCRJ3jaDM/vuVD3DsED6QMmSoplcdTAwMTLSXHUwMDAxu1x1MDAxZj/XytjN3IZljlx1MDAwMcegrmBcdTAwMTK4nYmhR+ya42ZAxNFcdTAwMDKBk1x1MDAwMlD7hoKw2kM+JFx1MDAxY5RiRiq7ZT5nXHUwMDEw31ODW9/h5i1vjV1mLJtcdTAwMDE4wM7+4kiJhGG2VaPvSVx1MDAxYeyJSKyu5VxiL1x1MDAwNONN67ehWfrowDBbXHUwMDAzKkBSXHUwMDEwOliCQlh8IzTlIFx1MDAwMCuqXHUwMDA3RGU1gSzRi4OTo/47L1x1MDAxOItsZVx1MDAxN+NklGKiYDO8TTF9ouiyolx1MDAxOJNcdTAwMGV7fte1XHUwMDA0VFx1MDAwNsNbPVtcdTAwMTSzkk4pad/LrNiIXfNDMbxcdTAwMTFcYlRnRnOmkVSNQzHlgFx1MDAwNo2pXHUwMDA1qN2bKbZcdTAwMDSKoZnYZvxvRsimXHUwMDA2seC9YkhcdTAwMDFcdTAwMDBZqaaMKjlmL0dKXHUwMDFkgdyIXGIuUPNgXHUwMDEye+N2s+lzI0esYsq+XCJcdTAwMDVcdTAwMThcdTAwMTNKo9aKb2kmXHUwMDFkXGZuJIlcdTAwMDSI3Ss0btMqYdl6ojPbT8yNk8Ds08tcctbcdtv2dnmD1kC/rpVf+lx1MDAwMMOnXFy7r3n9zXjc/XJcdTAwMTN8bMdXUJ9cdTAwMTaJPPusf/716a//XHUwMDAxsk3fXHUwMDAxIn0= App()Screen()Header()Footer()Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")Static( QUESTION, classes=\"questions\")

    Here's the output from this example:

    ExampleApp \u2b58ExampleApp Do\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    You may recognize some elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.

    "},{"location":"guide/CSS/#css-files","title":"CSS files","text":"

    To add a stylesheet set the CSS_PATH classvar to a relative path:

    Note

    Textual CSS files are typically given the extension .tcss to differentiate them from browser CSS (.css).

    dom4.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\n\nQUESTION = \"Do you want to learn about Textual CSS?\"\n\n\nclass ExampleApp(App):\n    CSS_PATH = \"dom4.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Footer()\n        yield Container(\n            Static(QUESTION, classes=\"question\"),\n            Horizontal(\n                Button(\"Yes\", variant=\"success\"),\n                Button(\"No\", variant=\"error\"),\n                classes=\"buttons\",\n            ),\n            id=\"dialog\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = ExampleApp()\n    app.run()\n

    You may have noticed that some constructors have additional keyword arguments: id and classes. These are used by the CSS to identify parts of the DOM. We will cover these in the next section.

    Here's the CSS file we are applying:

    dom4.tcss
    /* The top level dialog (a Container) */\n#dialog {\n    height: 100%;\n    margin: 4 8;\n    background: $panel;\n    color: $text;\n    border: tall $background;\n    padding: 1 2;\n}\n\n/* The button class */\nButton {\n    width: 1fr;\n}\n\n/* Matches the question text */\n.question {\n    text-style: bold;\n    height: 100%;\n    content-align: center middle;\n}\n\n/* Matches the button container */\n.buttons {\n    width: 100%;\n    height: auto;\n    dock: bottom;\n}\n

    The CSS contains a number of rule sets with a selector and a list of rules. You can also add comments with text between /* and */ which will be ignored by Textual. Add comments to leave yourself reminders or to temporarily disable selectors.

    With the CSS in place, the output looks very different:

    ExampleApp \u2b58ExampleApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aDo\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS?\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aYesNo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/CSS/#using-multiple-css-files","title":"Using multiple CSS files","text":"

    You can also set the CSS_PATH class variable to a list of paths. Textual will combine the rules from all of the supplied paths.

    "},{"location":"guide/CSS/#why-css","title":"Why CSS?","text":"

    It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your .py files?

    A major advantage of CSS is that it separates how your app looks from how it works. Setting styles in Python can generate a lot of spaghetti code which can make it hard to see the important logic in your application.

    A second advantage of CSS is that you can customize builtin and third-party widgets just as easily as you can your own app or widgets.

    Finally, Textual CSS allows you to live edit the styles in your app. If you run your application with the following command, any changes you make to the CSS file will be instantly updated in the terminal:

    textual run my_app.py --dev\n

    Being able to iterate on the design without restarting the application makes it easier and faster to design beautiful interfaces.

    "},{"location":"guide/CSS/#selectors","title":"Selectors","text":"

    A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.

    Selectors can target a kind of widget or a very specific widget. For instance, you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.

    Let's look at the selectors supported by Textual CSS.

    "},{"location":"guide/CSS/#type-selector","title":"Type selector","text":"

    The type selector matches the name of the (Python) class. For example, the following widget can be matched with a Button selector:

    from textual.widgets import Static\n\nclass Button(Static):\n    pass\n

    The following rule applies a border to this widget:

    Button {\n  border: solid blue;\n}\n

    The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Button Python class extends Static.

    Static {\n  background: blue;\n  border: rounded white;\n}\n

    Note

    The fact that the type selector matches base classes is a departure from browser CSS which doesn't have the same concept.

    You may have noticed that the border rule exists in both Static and Button. When this happens, Textual will use the most recently defined sub-class within a list of bases. So Button wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, the buttons would be \"solid blue\" and not \"rounded white\".

    "},{"location":"guide/CSS/#id-selector","title":"ID selector","text":"

    Every Widget can have a single id attribute, which is set via the constructor. The ID should be unique to its container.

    Here's an example of a widget with an ID:

    yield Button(id=\"next\")\n

    You can match an ID with a selector starting with a hash (#). Here is how you might draw a red outline around the above button:

    #next {\n  outline: red;\n}\n

    A Widget's id attribute can not be changed after the Widget has been constructed.

    "},{"location":"guide/CSS/#class-name-selector","title":"Class-name selector","text":"

    Every widget can have a number of class names applied. The term \"class\" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag will share styles.

    CSS classes are set via the widget's classes parameter in the constructor. Here's an example:

    yield Button(classes=\"success\")\n

    This button will have a single class called \"success\" which we could target via CSS to make the button a particular color.

    You may also set multiple classes separated by spaces. For instance, here is a button with both an error class and a disabled class:

    yield Button(classes=\"error disabled\")\n

    To match a Widget with a given class in CSS you can precede the class name with a dot (.). Here's a rule with a class selector to match the \"success\" class name:

    .success {\n  background: green;\n  color: white;\n}\n

    Note

    You can apply a class name to any widget, which means that widgets of different types could share classes.

    Class name selectors may be chained together by appending another full stop and class name. The selector will match a widget that has all of the class names set. For instance, the following sets a red background on widgets that have both error and disabled class names.

    .error.disabled {\n  background: darkred;\n}\n

    Unlike the id attribute, a widget's classes can be changed after the widget was created. Adding and removing CSS classes is the recommended way of changing the display while your app is running. There are a few methods you can use to manage CSS classes.

    • add_class() Adds one or more classes to a widget.
    • remove_class() Removes class name(s) from a widget.
    • toggle_class() Removes a class name if it is present, or adds the name if it's not already present.
    • has_class() Checks if one or more classes are set on a widget.
    • classes Is a frozen set of the class(es) set on a widget.
    "},{"location":"guide/CSS/#universal-selector","title":"Universal selector","text":"

    The universal selector is denoted by an asterisk and will match all widgets.

    For example, the following will draw a red outline around all widgets:

    * {\n  outline: solid red;\n}\n
    "},{"location":"guide/CSS/#pseudo-classes","title":"Pseudo classes","text":"

    Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the :hover pseudo selector.

    Button:hover {\n  background: green;\n}\n

    The background: green is only applied to the Button underneath the mouse cursor. When you move the cursor away from the button it will return to its previous background color.

    Here are some other pseudo classes:

    • :disabled Matches widgets which are in a disabled state.
    • :enabled Matches widgets which are in an enabled state.
    • :focus Matches widgets which have input focus.
    • :blur Matches widgets which do not have input focus.
    • :focus-within Matches widgets with a focused child widget.
    • :dark Matches widgets in dark mode (where App.dark == True).
    • :light Matches widgets in dark mode (where App.dark == False).
    "},{"location":"guide/CSS/#combinators","title":"Combinators","text":"

    More sophisticated selectors can be created by combining simple selectors. The logic used to combine selectors is know as a combinator.

    "},{"location":"guide/CSS/#descendant-combinator","title":"Descendant combinator","text":"

    If you separate two selectors with a space it will match widgets with the second selector that have an ancestor that matches the first selector.

    Here's a section of DOM to illustrate this combinator:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSFx1MDAxMn7vX+HwPOzsRKOp+5iIiVxyfODbbjdu293bXHUwMDEzXHUwMDBljGSQXHUwMDExXHUwMDEylmSwPTH/favARlx1MDAwMiRZXHUwMDFjYmCnefAhQSmVyuP7KiuLPz9sbGyGz1x1MDAxZGvzt41N66lec2zTr/U2P+rjXctcdTAwMGZsz1WnUP//wHv06/13NsOwXHUwMDEz/Pbrr+2a37LCjlOrW0bXXHUwMDBlXHUwMDFla05cdTAwMTA+mrZn1L32r3ZotYP/6J+ntbb1e8drm6FvRFx1MDAxNylZplx1MDAxZHr+4FqWY7UtN1xm1Oj/Vf9vbPzZ/1x1MDAxOZPOt+phzW04Vv9cdTAwMDP9UzFcdTAwMDFcdTAwMTEg44dPPbcvLVx1MDAxNFx1MDAxY1x1MDAwYimAkMN32MGOumBomer0nVx1MDAxMtqKzpj9a1x1MDAwNVx1MDAwN7x5b1Pil1x1MDAxOWx/2nX2v5Ur0XXvbMephs/OQFx1MDAxN7V689GPSVx1MDAxNYS+17KubDNs6quPXHUwMDFkXHUwMDFmfi7wlFx1MDAxYaJP+d5jo+laQTDyXHUwMDE5r1Or2+GzPlx1MDAwNsDw6EBccr9tREee1H+UY4MxTlx1MDAwNGFcdTAwMDBxQfDwrP48ZtJcdTAwMDBEQCoxoOokXHUwMDE5k2vbc9SzUHL9ZDHOXHUwMDAxiCS7rdVbXHIlnmtcdTAwMGXfXHUwMDEz+jU36NR89cSi9/Xe7pix4bGmZTeaoTooRHQ9q693iFx1MDAwMFaCMFx1MDAxYd2FvkrnwOxcdTAwMWLBXHUwMDFm43pr1vzOq342XHUwMDAz/U9MQi3c7rhcdTAwMDXFrSj2bKl7V7rCsve1aVx1MDAxZcKb26dcdTAwMTPGYDBcdTAwMWNrxORC6yncXHUwMDFjnvjrY9aw7W9fji727ra64ZfLw9557+Wk0blNXHUwMDFltub7Xi/vuFeHl93eMdk5OfapufeMXHUwMDFlXHUwMDEx2WFcdTAwMGJcdTAwMThcdTAwMTddmVx1MDAwN/uVVv1ElFx0vGg7Z7vut8ZcdTAwMDLGLUi96zXs8ePOPpDXXHUwMDA3L0934qhSMZ9uat3eXHUwMDBm5S5m2H1rXHUwMDBmXHUwMDA33bpdvtm+XHUwMDBi7ytcdTAwMDe75fOr47mUO1wixce8N5JT3Obxdlx1MDAwZmBcdTAwMGK07zudg6Pj7svN+U6YT9zXv6JQ+Ngxa4OspYIsJJBcIiZcdTAwMDSPwq1juy110n10nOiYV29Fie5DTOCJXHUwMDE0O3L/I9lcdTAwMTWK8cNv2ZVzKiHjMMpZ7yXXbLNY2eQqspKrIFx1MDAwNl1OcqVcdMmV8vHkilR2RVx1MDAxY9LosS0sty7SXHUwMDFho4fuuWHVfunDNTZytFJr287zyHPrm6mSdFudrtmu5f/83dUnbPP375umXXO8xvfN7+6/41pcdTAwMGUsJY1cdTAwMWWeopFxyo7d0Ca+6Vh3o7ZcdTAwMWbaXG6oXHUwMDBlT7dt04xDz/rbtVx1MDAwZvJcdTAwMDBGz7dcdTAwMWK2W3Mu8kqe6abZSJhClOarkCtcdTAwMTAsOVx1MDAxMjy3s55fPDjWS3nr8XHr5MA5Obt2ryv3f6+z0nd9lVx1MDAwMYNCXHRcYodcdL5KMTOkgJwqV9W+SlN9XHUwMDE19F9z+KpcdTAwMDSTvirYhK9cdTAwMDKAMYNAXHUwMDE24KxZiem+etjYvfDc7Va9xfyn1sG31kNKYvpcdTAwMDGEp1x1MDAxZrcg9a7XsEVcdTAwMDHh9dLChd/c6d12rYtcdTAwMTBcdTAwMWY6XHUwMDEwiZfWeWc+xLqOWlg8blx1MDAxZoyLn1x1MDAxZkpfkajUQ9JcdKrW887Z9lx1MDAwM1/AuGZcdTAwMWJcXG57h597leujbafb8XdvXHUwMDFiuFCekaz4aNjXvzJcdTAwMTFcdTAwMThmJMr/RfFcdTAwMDGKU/lcdTAwMDCkilx1MDAwYlxiRkGUV9/DXHUwMDE42fa2qlx1MDAxOINmYVxmXG5ccrJcdTAwMWOMIVx1MDAxMjDGJFx1MDAxZpCAXHUwMDEzIFxiL2CubZHWOFx1MDAxZlx1MDAxZthXMPtFI2vnZ318XHUwMDAwretOLVxirEDh69vHMPTcYNnU4Fx1MDAxZFx1MDAwND1OXHKmuYlM581mXHRcXKbOlzPMXHUwMDA1Izj/bPmX8PzCe3q+2eptnZ1+uXj5/PKpzFL8d8xcdTAwMGb/LjpPODGApFxiXG7JuXJfQkb8lyNqKCpcdTAwMGJcdFx1MDAxM8rJXHRcdTAwMTfz+O9PXHUwMDEw3VxuoajxzFxmXHUwMDAxXHUwMDEyrKRcdTAwMDScocW7b1ZcdTAwMDZkrOneyy77elolnYY8wkfHj5X5gcBcdTAwMGaGUKh612vYolx1MDAxOMJ6aaEohrBeWnDk1ra43dsm24xZ1Yujp0alssK2sHyCkHwjOcWtXHUwMDFmnV6QzrF/+tXfvjWt44483vuyuELEUoiHiNWWx4lcdTAwMDeTXHUwMDEwXCKJRX7ikW1cdTAwMTcrWokgXFxmQlx1MDAxNypccrwo6DI99YBcdTAwMTFsfJvepLpARPCyS1x1MDAxMUulXHUwMDFlW31Y/vP3za+WwubJ9Fx1MDAwMpKRj1xy+UNd3Y7lz04w3oHf41x1MDAwNGNcXNRMR8wmXHUwMDExXCK2pGbMXHUwMDFiXHUwMDExXHUwMDA0XGZDOUVZkMPKWYvsud3mXvUlaODnXHUwMDEz379ebVx1MDAxNsEkMqBSXHUwMDAye3XF0UlcdTAwMDBNXCKo1GW41SBcdTAwMTFAeyElYMkk4iGQlaP2WZVcdTAwMWOet2jFNe/RzqfLXHUwMDFmJGJRJKIg9a7XsEWRiPXSQlEkYr20UFx1MDAxNIlYLy0svijyXHUwMDFlN0m+kWjY17/+flx1MDAwZSFlOodAurTBXHUwMDEwyT/5mf38VpRDMMmzgFx1MDAwYtVcZmNBwGVcdTAwMTFcdTAwMTSCS0VcdTAwMWZcYohhmv9nXG5x6i2bQbxcdTAwMDO9U1x1MDAxOcRA0kwvXHUwMDFjXHUwMDA0l1x1MDAwNDdkIL2EXGIh1oYp83thdm15NUuITFx1MDAxN1x0XHRjXHUwMDE0YFwigEQjPkhcdTAwMThVJ1x1MDAxMZVcdTAwMTJDKinjhfkgMDBcdTAwMDZUcixcdTAwMTCmXGIozdNJn2TSgMopKIVCxVx1MDAwZS7wuItcIlx1MDAwMDliKFZVyu2ifVlndVHC4UxcdTAwMGJcdTAwMGWDsOaHW7Zr2m5DnYwy3VsnSp51fX2nrj9cdTAwMDZ9LVx1MDAwMkY5xVxcqVx1MDAxMFx0XHUwMDE5X1x1MDAwM6p1UetoTmZgSVxiXHUwMDAxQFExiFV4fX3DMONuWq75vkzZXHUwMDA1xZhMJSVcdTAwMTTmXHUwMDAwqStcIlxuoZIv8rmhUMjAnDMpKEVcdTAwMTLolDAhlFNcdTAwMGLCba/dtkOl+0+e7YbjOu4rs6w9vWnVzPGz6qbi58ZDQkePOMpcIqO/Nlwin+n/M/z7j4/J7043Zv2aMONovFx1MDAwZvHfU0czXGJcdTAwMDFcdTAwMWU/PKynXHUwMDAySFx1MDAwNIQyesN70SxcdTAwMWK8rSqmYMTgXHUwMDAyMimFJJLEVozrzzNMXGYqMUOIUPVcdTAwMDY2LtdcdTAwMDIxXHUwMDA1xFx1MDAwNkZQXHUwMDFiPEbKolGk92h2XHUwMDA0XHUwMDE4XGZzhFx0xFx1MDAwNKugXHUwMDE1W58xXGJnXHUwMDAyS6DcgswwVzJXOJtcdTAwMTVx5FxmZ7lDXHUwMDA3MFxiplx1MDAxOCCgMCDAkkNcdTAwMWPzo9fIXHUwMDAxocH0Ulx1MDAxZklcdTAwMDBcdTAwMDE6XHUwMDE0z1x1MDAxNs6ywceoTFxiS8l1L1x1MDAxZmBcdTAwMDKDJJlUXHUwMDAwoFKrknKOXHUwMDEw4utcdTAwMWTO0m1ZvyaseFHRLD5tO4HNJCfKmfNcdTAwMDez7CrZqlx1MDAwNjMsXGaKXHUwMDEwg0JiXHUwMDE1zMZjXHUwMDE5M1x1MDAwMMVcdTAwMWF8MJX1i1x1MDAwYmVSKmvGkimszFWWTmr9XHUwMDEwKuhcIqFbXFxcdTAwMTGEKn5MrPyCXG6VIKpS/z81lJU0JiCSqidJmYLZQrCYXHUwMDE3vcVccmyoUKf8jFx1MDAwM4XFlabfkMGUwSy7XHUwMDE2MyqVTodSuVx1MDAxM0QqpElcdTAwMDQnhKJcdTAwMDZcdTAwMDRUhVfOgZZrUqR1XG5lpVRj1q9cdDOeMpRl1qlcdTAwMTQySVxyZyq39d08f9HYadbA2XFnL7Shf0rK7Z3wyjxdcapJdEfMSFxmQ0hcdTAwMThQQlx1MDAwMlx1MDAxOCpwcSpNoJKCXHUwMDFiiKtcdTAwMDTCJ0BcdTAwMTfGkGDFhJfcXHUwMDEw7jlP1XLrwobynpZcdTAwMWN0ePxcdTAwMTLUq/PPwJ6cXHUwMDFmvHytPF2H/o5ValaDa47I3T+wQFWQelx1MDAwYlx1MDAxYZZ37f3Kwy5s+7s35k7YRd0zZ1x1MDAxMVogwO9At1s/vapcdTAwMWOdiqudK+8kPFpd7d7et8ufnC6DNCxtmbJxdKqyWqHlg+RcdTAwMWLJKe5cdTAwMWM91pnjfjmzzmXpwWqJ3a8v95/cs8vSXU5reEtb70CkXGJHXHUwMDE3VO6gNH2elWFFXHUwMDFmXHUwMDE0ssjfXHUwMDBlmm1uq5r86Hjyo9JcdTAwMTBcbm5cdTAwMTSZ+khC6kNcdTAwMTPTpopaKFx1MDAxNlFExluk4UXPNypsIDByNKOwUa37luX+nFLS4CPvX1hJ41x1MDAxZJQ2XtJcdTAwMTjKmOljqYSZp1ZcdTAwMTSFXHUwMDAy+EDPUud2sexYtpouRlx0MahmWFx1MDAxYzLEYUxcdTAwMWT93Vx1MDAxMaA0KKSFXCJNXGJcciggZlSRKSooTZj0IypcZqQgT6hcdTAwMWVRX+5cdTAwMTkqjPOWL/gsjpiTJGd7wUZ8bo1KrtNcdTAwMDElkjCMXHUwMDEyKDIxXHUwMDAwXHUwMDA1XFxcdTAwMDfN1zNTUuMpSilMPVx1MDAwZu04XHUwMDA0ci6jRqS4KEA339F+c1x1MDAxZJZyQqJ1YsbptqtfMauNXHUwMDA2+lx1MDAxMP8929pNlE6KXHTlXGIjPsVcdTAwMWPf+YV74MnHo93OTfPo7vzm0uJcdTAwMGbWiscsjJShgdGwNNgoTShwQFx1MDAxMFx1MDAxNJz2o1x1MDAxMlx1MDAxYpNogVEr305pSGFcdTAwMDTlnXyG2upcXFx1MDAxYqV99tF1q/n5oXlwWeru032zu3OfXGZ+fyzcnH7cgtS7XsNcdTAwMTa2UdpaaaGgYYvacKEgcVx1MDAxN0/i393XLfFGcor7cIvLVVRmdyc7XHUwMDE31WvvM7l7MlvrNTeAkEhdtVx1MDAwMLFUp5mg+SdcdTAwMDey7WJVUVx1MDAwME1GXHUwMDAxglx1MDAxOHhJKCDflm5QXGJcdTAwMDSgXHUwMDEyqID+jaInXHUwMDBi5tzTLbBN67bmL3/nhkxUm2tTt1x1MDAxMdHngOtZJXmqWFx1MDAxOVx1MDAwNGCKXd26veolsvDR4fVpq1x0fdn4dmydpHhq3feCoNSshfXm3++tXHUwMDEwXHUwMDE5irlob1x1MDAwNWNe2f88YobgXHUwMDA1zjJwPumpXHSdVlxiXCLMgIRL3tCtVVx1MDAwNtf7N4dP5llcdTAwMGaf7l1cdTAwMTDzmJXdXHUwMDFmgH1RgL0g9a7XsEVcdTAwMDH29dJCUZ1W66WFojqtXG5cdTAwMTJ38ds1XHUwMDE0JO57tCX5gjmlnYO2ZI5bpdVnSlx1MDAwZt2DXHUwMDFitoPuXHUwMDFliLtXPkwhhStLh6RM7WdXUiBAOEf5XHUwMDBiOdl2saJ0XGLyTIBFoIGLXHUwMDA0WCxcdTAwMDFgTVIhLplUkFx1MDAxN69h2XTqfjB9bMAnvm9cdTAwMWW4QVhznGXzoHfYQkp7WKrgmb6Z3i+W3rQpOVx1MDAwMphROMWOdZnrOlbTNSlcdTAwMTaGXHUwMDE4W7XwVl3tN1x1MDAxY1x1MDAxNemXRK/PXHUwMDFjxIVJXHUwMDA3JdKgjI0tJVx1MDAxY+5Dj1VUXHUwMDE1s1CheVdcdTAwMWbP5KqLLqyWgMGERIxJXHUwMDE1WpmEOKr3bkTlTIIxlVGFL6WyOir/XHUwMDFhVThLSfajXzHLicb4XHUwMDEw/z11nIgtZlx1MDAxYe/DYlxcb6RI85c1s7HSaoZcdFx1MDAwMqhBqMBcdTAwMTKqZIpZbHnBoK2UXHUwMDE4sexe5LQm0F83JaTuXHUwMDFml0xFZ0FcdTAwMTJyO4OGnmIl8vVcdTAwMTX74ozXuVx1MDAxNEgp5ZLPsjn+Klx1MDAwN5DsucXRXHUwMDAwotuwXHUwMDAwJZjplUSQxL4oYFx1MDAxOEK4IbBKglx1MDAxMFxyNJnQK5BrjUZ2pt9cdTAwMThpd1VcdTAwMDJJSlx0XHUwMDEyglx1MDAwYoZcdTAwMTPaXSfbW9cpZmWYr35NXHUwMDE47pTxK70kk1qRwSp5UEbzdypcdTAwMWP4n+Wlc/FcYus9XHUwMDFmXHUwMDA1l1x1MDAwN95J2fs8+ywvXHUwMDFh97VcdTAwMWMhLIpN03RfIVx1MDAwMlxmoWyLxSdz+ztTXGJmXGIwWIU0T+D66a5GXHUwMDExRYm7aaHoPqOFm5MrxpBe76ZcdTAwMDRZ8qKM4jdxXFzewtC2traNsGlcdTAwMDVWXCKbiYHGadhM6HXSqMzIrYzzlrg4s2FcdTAwMGYq0796R1LdcVx1MDAxZW98fs99s1x1MDAxZvVS3He25knMXHUwMDEzvLTvvlx1MDAxMlx1MDAxOFhcbiwwXHUwMDAzXHUwMDFjXHUwMDAzmr6pRY6vycrwYa5cdTAwMThcdTAwMTFcdTAwMWWjRNH2MsRQaVx1MDAxMlx1MDAwMVxyyrnKp5N7WWi6XCJcdTAwMTBe9lZcdTAwMTZcdTAwMDUjjuxsMJLbXHUwMDExXHUwMDAwXHUwMDEyXHUwMDEzhiBVhEVSjmLvXHUwMDFh9ktKQvHMi0Fz90lqYaR6XoLrfnxAJUlo3uRcbvFcdTAwMTJFoF43ROVcdTAwMTMyrVx1MDAxM/BIMF79Kk3a7YIgXHUwMDA3lOlcdTAwMWRcIpwjpU+FP3NHLeubXHUwMDBm7ZOtW8e8XHUwMDExqGe13Fx1MDAxNv+yv1x1MDAxNqBcdTAwMDNcdTAwMTNcdTAwMTW1JjGHOmAw2bf1oiCHiEJNXHUwMDA25GBSXHUwMDExOimW/SVcdTAwMDCrNbE/XHUwMDFm4vjF9Nx/hb9svKV6O1hcdTAwMDXgkSDVbPhcdTAwMDPFZsTH9+NcdTAwMDVUXHUwMDA1T8R5fk/OfvCrjD9cdTAwMTAxMMaC6u2zIIjvXHUwMDEw1XdoXHUwMDE1YYnsr+wqXGJ/MGlwyoWKXHUwMDE5XHUwMDEyYUXdo4npqKjBjMGU31x1MDAwNLFcdTAwMTCMU5XIwLJbUVxuhlx1MDAxZtl5YWNkwlx1MDAwMynlYaQ3RVx1MDAxMIIgNDndIYz+ZMeM4CP3LIdcdTAwMTJF51lcYlx1MDAwMVwiXGbr73lcdTAwMTJsQlx1MDAxNjiQN0madYJcdTAwMWSpNqtfpaG5pmGOXHUwMDBmr1x1MDAwM2/WOp1qqGxrqH9lvrb5XHUwMDFhqaO72+zaVm8ryav6L1x1MDAxZFx1MDAwMPt61GHG0vf4519cdTAwMWb++lx1MDAxZow/wb0ifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")match these*don't* match this

    Let's say we want to make the text of the buttons in the dialog bold, but we don't want to change the Button in the sidebar. We can do this with the following rule:

    #dialog Button {\n  text-style: bold;\n}\n

    The #dialog Button selector matches all buttons that are below the widget with an ID of \"dialog\". No other buttons will be matched.

    As with all selectors, you can combine as many as you wish. The following will match a Button that is under a Horizontal widget and under a widget with an id of \"dialog\":

    #dialog Horizontal Button {\n  text-style: bold;\n}\n
    "},{"location":"guide/CSS/#child-combinator","title":"Child combinator","text":"

    The child combinator is similar to the descendant combinator but will only match an immediate child. To create a child combinator, separate two selectors with a greater than symbol (>). Any whitespace around the > will be ignored.

    Let's use this to match the Button in the sidebar given the following DOM:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPayFx1MDAxNv2eX+HyfMmrXHUwMDFhNL0vUzX1ylx1MDAxYl6It1x1MDAxOMdxXqamXHUwMDA0yFi2QFjIYDw1//3dxlx1MDAxOFx0kFx1MDAwNDiIQCaaqlx1MDAxOCTRurp9l3P6dvf8/W5jYzPstZzN3zc2naeq7bm1wO5u/mrOd5yg7fpNuET639v+Y1Dt33lcdTAwMWKGrfbvv/3WsIN7J2x5dtWxOm770fba4WPN9a2q3/jNXHKdRvu/5t9cdTAwMTO74fzR8lx1MDAxYrUwsKKHXHUwMDE0nJpcdTAwMWL6wcuzXHUwMDFjz2k4zbBccq3/XHUwMDBmvm9s/N3/NyZd4FRDu1n3nP5cdTAwMGb6l2JcdTAwMDJcdTAwMTLKx0+f+M2+tJgxxKhiWFx1MDAwZe9w27vwwNCpweVcdTAwMWJcdTAwMTDaia6YU5unT1x1MDAwZr3CcfehXFyq1iv+x6B81Ph4XHUwMDE1PffG9byLsOe96MKu3j5cdTAwMDYxqdph4N87V24tvDVPXHUwMDFmOz/8XdtcdTAwMDc1RL9cbvzH+m3TabdHfuO37KpcdTAwMWL24Fx1MDAxY0fDky9a+H0jOvNkblDIYkQxoZSgSpPhRfNrirhFJOiAIcSU0HxMqlx1MDAxZN+Dnlx1MDAwMKl+Qf0jkqtiV+/rIFxcsza8J1xm7Ga7ZVx1MDAwN9Bf0X3d1/dcdTAwMTVieO7Wceu3IZxUKnqe09e6VlJcdTAwMTKCWNQn5iGtw1rfXHUwMDAy/lx1MDAxY1farVx1MDAxZLRcdTAwMDbK2WybLzFcdTAwMDGNbHvj5lx1MDAxMzehWMdcdTAwMWU3gkcquttb+1x1MDAwNz1153/aqYf+zrCtXHUwMDExe1x1MDAwYp2ncHN44Z9fs5ptfLkslfdvtjvh5aej7nn3+bjeqiQ3a1x1MDAwN4HfnbXdq6NPne5cdTAwMDe2e/wh4LX9XHUwMDFleSRsVyygXXJVOzwo3leP1Vx1MDAxNsPlhne61/xSX0C7Oal3vZr98Lh7gPTnw+enXHUwMDFiVSpcdTAwMTZrT3/Zne5P5S6m2eYneVx1MDAxM1xcf9zzd0J9dl1raX5Fb1ZXucGjLu70xNlflza9Pqtf1vjV0dk3iTtcIsWvs75I1OzgU1x1MDAxNGFcdTAwMWZbNfslXHUwMDEzQuiG5KBcdTAwMDX8p/Twuuc27+Fi89HzonN+9T5Knu9i8k6k7Vx1MDAxMTlHMjZT46eHXHUwMDE5XHUwMDFicoMkSNCZXHUwMDEzdnb3rWrC5lx1MDAxOVx0myBLLidh84SEzaO8PEjYkKtcdTAwMDVcdTAwMTOYxSxjYVx1MDAxOXuRxlx1MDAxOPW531xmL9znvj2JkbNFu+F6vZFu61spSLpcdTAwMDOXbbfpXHUwMDA07782zVx1MDAwNbf2x9fNmmt7fv3r5tfmf+JqbjsgjWmek5F2tjy3bix803NuRk0/dFx1MDAwMftcdTAwMGUvN9xaLY5mq6/PPpxcdTAwMDWD+oFbd5u2V55V8kwvzVx1MDAwNtec41RX5eDHWjOGZ/bVx+1tzfZ6qHhyt9XcVzLsnlx1MDAwNEer7qtCWkQhRDGd9FUmqUVcdTAwMTlTXHUwMDA0XHUwMDEx3vfVXHUwMDFjnVWjSWdVYtxZmWZcdTAwMTBBOc7BV7Oy3e5D8S44Oz54OC/uljpN++7y41x1MDAxN/cnul5cdTAwMTS6zkm969VsXuh6vbRQXHUwMDBlbne7lY5TXHUwMDBl6ZGHiXq+P299+NdpIS8ycNCz5e1e62z3ktxXXHUwMDFmcIufXHUwMDFmXHUwMDFl11x1MDAxNtBuVVx1MDAxY5/xc9ooXHUwMDFl1IJS4aSwfVK8JavYa9NIRvJcdTAwMDOjZlx1MDAwN5++P8ngUoyfXHUwMDFlXCJcdTAwMTdcIjHVUszBMrL1vKLIRZJcZuSipKWWhFxcVFx1MDAwMnKZpFx1MDAxOVpSjaj6sVnGXHUwMDAxgPdng9e99+b8XHUwMDBiYK96drvttFx1MDAwMbVXXHUwMDFlw9BvtpdNOKbg8nHCMc9LZDpvNvdQKHVgXzGwXHUwMDEzhYme2YErXb3fKNRDXSn0XHUwMDBlnXapt394WllxXHUwMDA3ZmZcXJ9LKbg0fkFGPVhcYmJhLFx1MDAxOcKcI+M835t6SI6QknHGuFx1MDAxNOqx5fTY/vmhf1cv71x1MDAxNLYudz63y92Ugbaf1GP+dnNS73o1m1x1MDAxN/VYLy3kRT3WS1x1MDAwYp7e3lGV/Vx1MDAxZLYjhHNRLj3Vi8VcdTAwMTW2hbyYx8LFncY8klx1MDAxZlx1MDAxODU7+PT9mYfSLJV5IFwiIFczOfuYabaeV1x1MDAxNbjwLOCiuSWWXHUwMDAzXFySmEdsaPSVeSBcZlxmXGKxXHUwMDFjgMtcdTAwMTRrjFx1MDAwMau8mcd2XHUwMDFmlb//unntXHUwMDAwNE9mXHUwMDE3mI38bEhcdTAwMWaq8DpO8HZ+MVx1MDAwNXyP84txUTPdMJtDQNem+lwiVlxuXHUwMDBijsXsJOLx+aDTPH1+vmS03PN4VTS/PFx1MDAxN1fcXHUwMDE3wcgspFx1MDAxNXDrXHUwMDE3X1x1MDAxY+NcdTAwMTDYklpcIqXRanBcYkqxXHUwMDE2XFzFRiOWwiGuXHUwMDBlXHUwMDBmzz5cXD8/bFx1MDAxZlx1MDAxNC78QtdcdTAwMGWfPznHPznEojhETupdr2bz4lx1MDAxMOulhbw4xHppIS9cdTAwMGWxXlrIq9iycHGnUZPkXHUwMDA3Rs1cdTAwMGU+LVx1MDAxMlxmZmKiNGpCXHUwMDExkuOnI2pcIjlSfJ6iSLaeV1x1MDAxM1x1MDAwZUnEMuBcdTAwMTAwXHUwMDEzslx1MDAxYzg0XHUwMDFiM8GaKVx1MDAwMmLKXHUwMDFjJkuvIDU58ZfNTKYg+lRm8lwiaaZcdTAwMTO+hKxcdTAwMDQvlFx1MDAwNKU6IcdSKCb57E6YXVx0X00nXHUwMDA0hG9cdTAwMTFwQLAyjlx1MDAxMJd8xFx1MDAwYlx1MDAxOcKWkERx+MC0XCKxQu6i3Vx1MDAxMFlAOFx1MDAxMMZcXEumheI0NnQzdEuhISgwXCKgZ1x1MDAwNHiimvRSjLjCwHDo/F7aXHUwMDE3dtle2lx1MDAwZe0g3HabNbdZh4tRrntdjDPLPMS+X1dcdTAwMWbbfTVcIlx1MDAwMb1IJSeIKOgyLWN31e2W4XpcdTAwMTYolzGEjL4pkXhwwzDnbjrN2nSZskuVMZlcbiBcdTAwMTSViMBcdTAwMTNcdMdA9LlSfEIqYlEphVx1MDAwNsJJNIJAKyak8ux2uOM3XHUwMDFhblxiyj/z3WY4ruS+NreMt986dm38KrxV/Np4WGiZXHUwMDE2R/lp9Gkj8pv+l+HnP39NvjvdnM0xYchRe+/if+eOaJjg1MlcdTAwMTbgXHUwMDE1nDGsZ1x1MDAxZvHMhoUrXHUwMDFh0SS2OIGghSRYN4uiSP/XXHUwMDEyWVx1MDAxYZCVMkNcdTAwMWKaxIZDXHUwMDE3XHUwMDBlKzC1qFCKQWDlbGTSRzTogiymXHUwMDAwXHUwMDAyMZCTgj3EJn5cZuq4mijMwWt+rGg2c+RcdTAwMDD9UI6FMj5EXHUwMDA0pFx1MDAxZlx1MDAxNvOiQdzAkKKwXHUwMDE5NWaIIU5cdTAwMDR7YzTLhFx1MDAxZqMyXHUwMDExqjX4rEJIKFxuXHUwMDFkPSlcdTAwMTO4P9dcZnPCzVxuO1x1MDAxMHytg1m6LZtjwopcdTAwMTdcdTAwMTbLIGWkwjOALIRcbj7HvJPs8tuKXHUwMDA2M8aBIyHMTWKkMjZA/lx1MDAxMs2IxbAwI7VcXFx1MDAwYinHxVpcXDTTXHUwMDFh+phA0NRcdTAwMWNDOoutXHUwMDFhioJcdTAwMTmz4CqVcFx1MDAxZsZE0IlZZVx1MDAxOEJcdTAwMTlcdTAwMDNcdTAwMGbG/9ZoVjCggHFMXHUwMDAwalx1MDAxM1xiaYyihHBGLUhcXFx1MDAwMJOAXHUwMDFlQ1xu44K+LZ5lXHUwMDE3ekal4lx1MDAxYfA/1lx1MDAxMlx1MDAxM4hqmuBcdKG4XHUwMDA1uFx1MDAxYVwirJTIyDUp0jpFs0KqMZtjwoznjGaZRTBcdTAwMTnjJWNcdTAwMDGNaCyQmme1nfZKl6Wb+6fS9dVFrfaJ2Fx1MDAxN1x1MDAxZj6ueDhjZlx1MDAwNY8wuYJAzkYyXG4j/eFcYlx1MDAwMbpcdTAwMDeEKlx1MDAwMbVpXHUwMDA0d+RcdTAwMDfOYlWtKICBbPBoKidgXHUwMDE4QGZcdLQknmyWUlx1MDAwYrvrlu9cXO+ycdTuak4qXi0sep++fbD3+Pzw+br49DlcZnadwu1F+7MkbFx1MDAxMTP2161cdTAwMTaWk3pzalZ23IPiw1x1MDAxZW5cdTAwMDR7f9V2w1x1MDAwZemceovQXHUwMDAyQ0FcdTAwMGI3O9WTq2LpRF3tXvnHYWl1tVu5a2ydeVx1MDAxZIF5WNiu6XrpxH/qra64i197PlDD58NcdTAwMTLx2lx1MDAxN6p1etxmndZt6ZBfflO70yoryVxuipp9zY5cdTAwMGJEYpmJNq2yXHUwMDAySDiVNCDgYkQxOnuWzTaLXHUwMDE1zbJmtUl6ltXCgoSGXHUwMDAx5+mcsyxLyLIk0v1rdqVUamqw1uKza96VlVj9YEpl5aJcdTAwMWE4TvN9Sk1Fjty/sJrKXHUwMDE0jDheU1x1MDAxOcqY6XjpfF2lbyZBgD4gTdjs2z9lR87V9DzOsVx1MDAwNWRcdTAwMWNcXItcdTAwMTGqXGJcdTAwMWatpsB3S0klluB5XHUwMDE4XHUwMDAzi1SUaOCZSopYrXnoiIybMlx1MDAwMFx1MDAwN8ebwLtcdTAwMDQhRpjW4lxyeHeVmXq2O2zEx/i4llx1MDAwMuJcdTAwMTJcdTAwMDeWLijhMYo4oMTMQlx1MDAxY1RcZtpcdTAwMWJcXJmTn89R0Vx1MDAxMVx1MDAxOFx0XHUwMDA19JxhXHSBMqGgXHUwMDAzsiCgsohzbpKb1lx1MDAxM1wirVx1MDAxMz9PN15zxMw2auhd/O/bpqfSdGpcdTAwMGVsUYE7izm217huPV2497RyfcnK4fn+XHUwMDE26dyffl7x4EWJtKggXHUwMDFjSHpcdTAwMDJsMJvXYWVqwfqFnH/33eugQ1x1MDAxOFn+XCK34z1WPFdq9+amVun2yrf+VlhOISA/J6jO325O6l2vZnPbvW6ttJBTs7ntXpePuHmNIOQk7nn9oX7uXFw9XHUwMDEwLb94lyeNtt1yUsZRXHUwMDE2tdle4otEzb6Cg+89MEEoSV1cdTAwMDFDMCFCKI5nh1x1MDAxONn9t6JcdTAwMTCDkiyIQVx1MDAwMOsuXHRizLbfXHUwMDFlxpIjSlx1MDAwMX2v39DEN+6313ZrTsVcdTAwMGWWvf/FXHUwMDE04DzThnsjome66pRcdTAwMTVr6ZtjMsaAiDE++3DG9lHTx1xi6JdDjypP5Vx1MDAxMr7URSfFXcfcbtRZx6csvdVZMZrqrVhbXGZpRvCLt45OPmCCWIJcbo6Y+PbRjF8wqSgllEpwVZkweJGw5Vx1MDAwNSZcdTAwMTKx+Fx1MDAwNNalsIHzvaND9CRLN6dXLr66PdNfetf7P9nAothATupdr2bzYlx1MDAwM+ulhbyWq62XXHUwMDE28lqulpO4eW15sV6dtnxOlPxcIjOKe1rd3T65bqn60Ze7Y7vje0/bjM0m7uDTd+daXHUwMDE00dTldUT3t9mLIYVp2C3bLL5cdTAwMGLVmlx1MDAwMb1cdTAwMTGWhd6ktPCi0NuU0dxcdTAwMDT8lrC1uWRMXHUwMDAzXHUwMDA0zFx1MDAwMb+tXHUwMDBl01x1MDAxYSxaM+de+MrXzcNmO7Q9b9k8a1xuXHUwMDFkSVltlyp4ppOmXHUwMDE3jFx1MDAxOUl1Ulx1MDAwNCleUDVcdTAwMDfDyp5cdTAwMTKzmlx1MDAwM1winHFLMCp14oBcYuHCMsMgS3BSxixJiOCCJMzZYNriQjAkJkvFVCBwJPKWXUJ+iEpxwVxmWWlcIoSmXHUwMDEwZzWmOKk8yyjlmsoppeJR+deoYltIMlx1MDAxZnPEXGYnauNd/O/8MYOn1mk55cpUjGfP65m4bDUjXHUwMDA2w8Si4OuvQ6hcdCt2XHUwMDExNsaY91x1MDAxY1x1MDAxM2RcdTAwMTFuZvQogYmAXGJcdTAwMTCb2Fx1MDAxNa3XNZNcdTAwMTckVlx0U6qppGZcdTAwMDe2t+T7VVx1MDAwZVx1MDAxY9njl6OBXHUwMDAzXHUwMDExJFx1MDAxMWdUSCRcdTAwMTiOZlx1MDAxN1xmXHUwMDAzh7RcdTAwMTQliGIzXHUwMDE3XHUwMDA3joR1XHUwMDE3M001yc71XHUwMDFiI4uHQVx1MDAxY7PGiygzUYlINLlOd3K18DqFqnSrNUdkr3OGq3RcdTAwMWWSselcdTAwMTnHRFx1MDAxMqlm5yFS2yd35O7sYLdXrX/0xd72WUWkxKtcdTAwMTVcdTAwMTlDJlx1MDAxNFx1MDAwMI7Go9jhZb8z9mrd31TmyVx1MDAxODkmSdPeJiFcZlx1MDAwNtJcdTAwMDH2IN8y2+1bho5Xi2YnXHUwMDExm9lnt142a05gXGLLRnjrtjde9lx1MDAxZE+e6qpGfjz7VNfQb6WxmZHXXHUwMDFhpy7Jor1ccnxA0EhcdTAwMDVcdTAwMWbIJGQ5x+L67P5fSW+mWFlcdTAwMTC2zNbiXHUwMDE0Y6FGx1x1MDAxNFx1MDAwNMA9zNToXG6qMY92hJRv92hNLU0pXHUwMDE1ZjNcdTAwMWUmaGxCXnxcdTAwMTVX4lx1MDAxYS6zmo/QkZLdXHUwMDBmXHUwMDAxOLJzwijggNxcdTAwMGXAXHUwMDExXHUwMDExXGKMWkNATli4XHUwMDBl0VrNwlW+XHUwMDE5a1x1MDAxOHGkJmbSqlZcdTAwMDSeiVTiYliwMqzM/zvDbFx1MDAwMrXe2CPVes1ReDXcNOTxbtDupt1qXYRgZcNeXHUwMDAwQ3Zrg5BcdTAwMWS93GbHdbrbXHTuddM/TFx1MDAwNOyr0Vx1MDAwNFx1MDAxYce84t//vPvn/yxzRNUifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")Underline this button

    We can use the following CSS to style all buttons which have a parent with an ID of sidebar:

    #sidebar > Button {\n  text-style: underline;\n}\n
    "},{"location":"guide/CSS/#specificity","title":"Specificity","text":"

    It is possible that several selectors match a given widget. If the same style is applied by more than one selector then Textual needs a way to decide which rule wins. It does this by following these rules:

    • The selector with the most IDs wins. For instance #next beats .button and #dialog #next beats #next. If the selectors have the same number of IDs then move to the next rule.

    • The selector with the most class names wins. For instance .button.success beats .success. For the purposes of specificity, pseudo classes are treated the same as regular class names, so .button:hover counts as 2 class names. If the selectors have the same number of class names then move to the next rule.

    • The selector with the most types wins. For instance Container Button beats Button.

    "},{"location":"guide/CSS/#important-rules","title":"Important rules","text":"

    The specificity rules are usually enough to fix any conflicts in your stylesheets. There is one last way of resolving conflicting selectors which applies to individual rules. If you add the text !important to the end of a rule then it will \"win\" regardless of the specificity.

    Warning

    Use !important sparingly (if at all) as it can make it difficult to modify your CSS in the future.

    Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:

    Button:hover {\n  background: blue !important;\n}\n
    "},{"location":"guide/CSS/#css-variables","title":"CSS Variables","text":"

    You can define variables to reduce repetition and encourage consistency in your CSS. Variables in Textual CSS are prefixed with $. Here's an example of how you might define a variable called $border:

    $border: wide green;\n

    With our variable assigned, we can write $border and it will be substituted with wide green. Consider the following snippet:

    #foo {\n  border: $border;\n}\n

    This will be translated into:

    #foo {\n  border: wide green;\n}\n

    Variables allow us to define reusable styling in a single place. If we decide we want to change some aspect of our design in the future, we only have to update a single variable.

    Note

    Variables can only be used in the values of a CSS declaration. You cannot, for example, refer to a variable inside a selector.

    Variables can refer to other variables. Let's say we define a variable $success: lime;. Our $border variable could then be updated to $border: wide $success;, which will be translated to $border: wide lime;.

    "},{"location":"guide/CSS/#initial-value","title":"Initial value","text":"

    All CSS rules support a special value called initial, which will reset a value back to its default.

    Let's look at an example. The following will set the background of a button to green:

    Button {\n  background: green;\n}\n

    If we want a specific button (or buttons) to use the default color, we can set the value to initial. For instance, if we have a widget with a (CSS) class called dialog, we could reset the background color of all buttons inside the dialog with the following CSS:

    .dialog Button {\n  background: initial;\n}\n

    Note that initial will set the value back to the value defined in any default css. If you use initial within default css, it will treat the rule as completely unstyled.

    "},{"location":"guide/CSS/#nesting-css","title":"Nesting CSS","text":"

    Added in version 0.47.0

    CSS rule sets may be nested, i.e. they can contain other rule sets. When a rule set occurs within an existing rule set, it inherits the selector from the enclosing rule set.

    Let's put this into practical terms. The following example will display two boxes containing the text \"Yes\" and \"No\" respectively. These could eventually form the basis for buttons, but for this demonstration we are only interested in the CSS.

    nesting01.tcss (no nesting)nesting01.pyOutput
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n}\n\n/* Style all buttons */\n#questions .button {\n    width: 1fr;\n    padding: 1 2;\n    margin: 1 2;\n    text-align: center;\n    border: heavy $panel;\n}\n\n/* Style the Yes button */\n#questions .button.affirmative {\n    border: heavy $success;\n}\n\n/* Style the No button */\n#questions .button.negative {\n    border: heavy $error;\n}\n
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static\n\n\nclass NestingDemo(App):\n    \"\"\"App that doesn't have nested CSS.\"\"\"\n\n    CSS_PATH = \"nesting01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"questions\"):\n            yield Static(\"Yes\", classes=\"button affirmative\")\n            yield Static(\"No\", classes=\"button negative\")\n\n\nif __name__ == \"__main__\":\n    app = NestingDemo()\n    app.run()\n

    NestingDemo \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Yes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0No\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    The CSS is quite straightforward; there is one rule for the container, one for all buttons, and one rule for each of the buttons. However it is easy to imagine this stylesheet growing more rules as we add features.

    Nesting allows us to group rule sets which have common selectors. In the example above, the rules all start with #questions. When we see a common prefix on the selectors, this is a good indication that we can use nesting.

    The following produces identical results to the previous example, but adds nesting of the rules.

    nesting02.tcss (with nesting)nesting02.pyOutput
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n\n    /* Style all buttons */\n    .button {\n        width: 1fr;\n        padding: 1 2;\n        margin: 1 2;\n        text-align: center;\n        border: heavy $panel;    \n\n        /* Style the Yes button */\n        &.affirmative {\n            border: heavy $success;        \n        }\n\n        /* Style the No button */\n        &.negative {\n            border: heavy $error;\n        }\n    }\n}\n
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static\n\n\nclass NestingDemo(App):\n    \"\"\"App with nested CSS.\"\"\"\n\n    CSS_PATH = \"nesting02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"questions\"):\n            yield Static(\"Yes\", classes=\"button affirmative\")\n            yield Static(\"No\", classes=\"button negative\")\n\n\nif __name__ == \"__main__\":\n    app = NestingDemo()\n    app.run()\n

    NestingDemo \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Yes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0No\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u2503 \u2503\u2503\u2503\u2503\u2503\u2503 \u2503\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Tip

    Indenting the rule sets is not strictly required, but it does make it easier to understand how the rule sets are related to each other.

    In the first example we had a rule set that began with the selector #questions .button, which would match any widget with a class called \"button\" that is inside a container with id questions.

    In the second example, the button rule selector is simply .button, but it is within the rule set with selector #questions. The nesting means that the button rule set will inherit the selector from the outer rule set, so it is equivalent to #questions .button.

    "},{"location":"guide/CSS/#nesting-selector","title":"Nesting selector","text":"

    The two remaining rules are nested within the button rule, which means they will inherit their selectors from the button rule set and the outer #questions rule set.

    You may have noticed that the rules for the button styles contain a syntax we haven't seen before. The rule for the Yes button is &.affirmative. The ampersand (&) is known as the nesting selector and it tells Textual that the selector should be combined with the selector from the outer rule set.

    So &.affirmative in the example above, produces the equivalent of #questions .button.affirmative which selects a widget with both the button and affirmative classes. Without & it would be equivalent to #questions .button .affirmative (note the additional space) which would only match a widget with class affirmative inside a container with class button.

    For reference, lets see those two CSS files side-by-side:

    nesting01.tcssnesting02.tcss
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n}\n\n/* Style all buttons */\n#questions .button {\n    width: 1fr;\n    padding: 1 2;\n    margin: 1 2;\n    text-align: center;\n    border: heavy $panel;\n}\n\n/* Style the Yes button */\n#questions .button.affirmative {\n    border: heavy $success;\n}\n\n/* Style the No button */\n#questions .button.negative {\n    border: heavy $error;\n}\n
    /* Style the container */\n#questions {\n    border: heavy $primary;\n    align: center middle;\n\n    /* Style all buttons */\n    .button {\n        width: 1fr;\n        padding: 1 2;\n        margin: 1 2;\n        text-align: center;\n        border: heavy $panel;    \n\n        /* Style the Yes button */\n        &.affirmative {\n            border: heavy $success;        \n        }\n\n        /* Style the No button */\n        &.negative {\n            border: heavy $error;\n        }\n    }\n}\n

    Note how nesting bundles related rules together. If we were to add other selectors for additional screens or widgets, it would be easier to find the rules which will be applied.

    "},{"location":"guide/CSS/#why-use-nesting","title":"Why use nesting?","text":"

    There is no requirement to use nested CSS, but it can help to group related rule sets together (which makes it easier to edit). Nested CSS can also help you avoid some repetition in your selectors, i.e. in the nested CSS we only need to type #questions once, rather than four times in the non-nested CSS.

    "},{"location":"guide/actions/","title":"Actions","text":"

    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.

    "},{"location":"guide/actions/#action-methods","title":"Action methods","text":"

    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\nfrom textual import events\n\n\nclass ActionsApp(App):\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n    def on_key(self, event: events.Key) -> None:\n        if event.key == \"r\":\n            self.action_set_background(\"red\")\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    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\nfrom textual.app import App\n\n\nclass ActionsApp(App):\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n    async def on_key(self, event: events.Key) -> None:\n        if event.key == \"r\":\n            await self.run_action(\"set_background('red')\")\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    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.

    "},{"location":"guide/actions/#syntax","title":"Syntax","text":"

    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')"},{"location":"guide/actions/#parameters","title":"Parameters","text":"

    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 \u2014 because new_color is a variable and not a literal.

    "},{"location":"guide/actions/#links","title":"Links","text":"

    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.pyOutput actions03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\n\n\nclass ActionsApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    ActionsApp Set\u00a0your\u00a0background Red Green Blue

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

    "},{"location":"guide/actions/#bindings","title":"Bindings","text":"

    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.pyOutput actions04.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\n\n\nclass ActionsApp(App):\n    BINDINGS = [\n        (\"r\", \"set_background('red')\", \"Red\"),\n        (\"g\", \"set_background('green')\", \"Green\"),\n        (\"b\", \"set_background('blue')\", \"Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n

    ActionsApp Set\u00a0your\u00a0background 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.

    "},{"location":"guide/actions/#namespaces","title":"Namespaces","text":"

    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.pyactions05.tcss actions05.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('cyan')]Cyan[/]\n[@click=set_background('magenta')]Magenta[/]\n[@click=set_background('yellow')]Yellow[/]\n\"\"\"\n\n\nclass ColorSwitcher(Static):\n    def action_set_background(self, color: str) -> None:\n        self.styles.background = color\n\n\nclass ActionsApp(App):\n    CSS_PATH = \"actions05.tcss\"\n    BINDINGS = [\n        (\"r\", \"set_background('red')\", \"Red\"),\n        (\"g\", \"set_background('green')\", \"Green\"),\n        (\"b\", \"set_background('blue')\", \"Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield ColorSwitcher(TEXT)\n        yield ColorSwitcher(TEXT)\n\n    def action_set_background(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = ActionsApp()\n    app.run()\n
    actions05.tcss
    Screen {\n    layout: grid;\n    grid-size: 1;\n    grid-gutter: 2 4;\n    grid-rows: 1fr;\n}\n\nColorSwitcher {\n   height: 100%;\n   margin: 2 4;\n}\n

    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.

    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').

    "},{"location":"guide/actions/#builtin-actions","title":"Builtin actions","text":"

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

    • action_add_class
    • action_back
    • action_bell
    • action_check_bindings
    • action_focus
    • action_focus_next
    • action_focus_previous
    • action_pop_screen
    • action_push_screen
    • action_quit
    • action_remove_class
    • action_screenshot
    • action_switch_screen
    • action_suspend_process
    • action_toggle_class
    • action_toggle_dark
    "},{"location":"guide/animation/","title":"Animation","text":"

    Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.

    "},{"location":"guide/animation/#animating-styles","title":"Animating styles","text":"

    Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to styles such as offset to move widgets around the screen, and opacity to create fading effects.

    Apps and widgets both have an animate method which will animate properties on those objects. Additionally, styles objects have an identical animate method which will animate styles.

    Let's look at an example of how we can animate the opacity of a widget to make it fade out. The following example app contains a single Static widget which is immediately animated to an opacity of 0.0 (making it invisible) over a duration of two seconds.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass AnimationApp(App):\n    def compose(self) -> ComposeResult:\n        self.box = Static(\"Hello, World!\")\n        self.box.styles.background = \"red\"\n        self.box.styles.color = \"black\"\n        self.box.styles.padding = (1, 2)\n        yield self.box\n\n    def on_mount(self):\n        self.box.styles.animate(\"opacity\", value=0.0, duration=2.0)\n\n\nif __name__ == \"__main__\":\n    app = AnimationApp()\n    app.run()\n

    The animator updates the value of the opacity attribute on the styles object in small increments over two seconds. Here's what the output will look like after each half a second.

    After 0sAfter 0.5sAfter 1sAfter 1.5sAfter 2s

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    AnimationApp Hello,\u00a0World!

    "},{"location":"guide/animation/#duration-and-speed","title":"Duration and Speed","text":"

    When requesting an animation you can specify a duration or speed. The duration is how long the animation should take in seconds. The speed is how many units a value should change in one second. For instance, if you animate a value at 0 to 10 with a speed of 2, it will complete in 5 seconds.

    "},{"location":"guide/animation/#easing-functions","title":"Easing functions","text":"

    The easing function determines the journey a value takes on its way to the target value. It could move at a constant pace, or it might start off slow then accelerate towards its final value. Textual supports a number of easing functions.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1XWVPjRlx1MDAxMH7nV1DeVyzmPrYqlYJcdTAwMDVcdTAwMTJcdTAwMDJLXHUwMDBlczib2ofBXHUwMDFh21x1MDAxM8uSkMZcdTAwMDaW4r+nJVx1MDAxM48vwrnUblX0YGumZ3q+6e6vu3Wztr7e8Ne5bbxfb9irjklcXFxcmMvGRjU/tkXpslx1MDAxNESkXHUwMDFll9mo6NQr+97n5fvNzaEpXHUwMDA21ueJ6dho7MqRSUo/il1cdTAwMTZ1suGm83ZY/lj9XHUwMDFlmaH9Ic+GsS+icEjTxs5nxeQsm9ihTX1cdNr/gvH6+k39O4POXHUwMDE0RTZcdTAwMDFWT1x1MDAwN3BKL05cdTAwMWVlaY1cdTAwMTMjrjSjWuDpXG5X7sBR3sYg7lx1MDAwMlxcXHUwMDFiJNVU42Tvl7TZ/nX76rh1fDDO+4j+dNVcdKd2XZK0/HUysYLp9EeFXHLS0lx1MDAxN9nAnrnY9/812sz8dF9syj5cdTAwMDCYiots1OuntqwuXHUwMDFmkGa56Th/XV9cdTAwMDNNZ03aq5WEmStcdTAwMThRwSMqXHUwMDE0XHUwMDE3XGZRQZhUU2m1n2lcdTAwMWNxXHUwMDAxcsyRRIxcdTAwMTK5gOxDloBcdTAwMWZcdTAwMDDZO1Q/XHUwMDAx2rnpXGZ6gC+Np2t8YdIyN1x1MDAwNXgrrLu8uzOONGZIXHUwMDEwPpX0rev1fWVcdTAwMGXCXCIlg6C0tVx1MDAxM7DCmiuhUZBUXHUwMDA35vtxXHUwMDFkXHUwMDBin1x1MDAxN43YN0V+Z6tGXHJsXHUwMDA2bDXcnVx0pLB5lMdm4nQsXHUwMDA0x1xcXHUwMDEzWtlqKk9cXDpcdTAwMDBhOkqSMJd1XHUwMDA2K+Kk9Kbw2y6NXdpb3GLT+Fx1MDAxZUliSv8hXHUwMDFiXHUwMDBlnVx1MDAwN1x1MDAxOL9lLvWLK2q9W1WQ962JV2ielS2yIa80XHUwMDA28lRPeFtcdTAwMGbRUlx1MDAwZqbvnzdWrm4uebGevXNg2L02+3+78UTa4pkwXFzkraCaIaxkYPZDvKUnh1x1MDAwN7y9d/7n3uFheaJHgra3+t9cdTAwMDFvRUQgXHUwMDE2pdRaXHUwMDAwbdlcdTAwMTJvMaFIYES1XHUwMDE0mi9cdTAwMDB7PdpShFwiKSmjclx1MDAwNXFxJCFtUEyWuEs5Ykpr9sbUhdAgwY7/U3dmwVxuR1ZPM/jwifRcdTAwMDXr2pXsRfL+skuYpkpjXHUwMDFj8utD9G3r38nu2cfty7//6F1cZoa7R1x1MDAwM3aQvDJ9y1xm+o7Xr7pUXHUwMDAwNSWjeJagNX2VjJCQXFxTRojmSC9cdTAwMDBcdTAwMGL0VYJY3XlcdH1pJKSCUsqpRFqH8lx1MDAxZqovXHUwMDAyMFx1MDAwNHMhJWGKXHUwMDEyvURmgpnAXHUwMDAyIf6mbFZcbmCrXHUwMDAw+Htj85zsVamMMYso4lpqSoRcdTAwMTYqNLHV06QogsZcdTAwMTZxJjWD5lx0yVx1MDAwN/VpXHUwMDEyKVwiJHTDoJJzzOb0YYVcIkisWFIou1x1MDAxMlx1MDAwYvVwqrkn6Gp1y/H2xMTj7ZVflXjEfVmnSjpcdTAwMDRcbkPoKlx1MDAxZUo6O+3Wyadd0vz482D3tDX+1L44z/Hzklx1MDAwZV5kx1dLOuDsiDBcdTAwMDZdPuJcdTAwMDRcbvB8q89cdHiRay5cdTAwMTTl0PD/R855acvAXHUwMDAyrJBklrt7wYggirFw2UcnlbJcdTAwMWE8M6lQKlmIlCcklW6W+pb7Ulx1MDAwN1x1MDAxNJqb3TNDl1xcz/mtjtHKUG5oZy1Z2rpsVlx1MDAxZjdza7dcdTAwMTLXS+uqarvzwe1cdTAwMWR8XHUwMDE1T8U+m7l3XHUwMDA3zjagrthfyjdZ4XouNcnxLI5nsYrcW8zhq1x1MDAxMVPIXG6Pp9Vps9y/lGdcdTAwMTe90+ukvfOlO+52bfNbp1x1MDAxNYH0x1x1MDAxNaZcXCqFkObznTiQKVx1MDAxMlxcK2jEXHUwMDE5NFLQkn81XnHyKF5BktaKMvyMYv1cdTAwMTJesSrLvlx1MDAxOa/GJlx1MDAxOX1cdTAwMTPEmlx1MDAwMJkwa+2uXCI2TJ63PNhcdTAwMDdcdTAwMTZMeFx1MDAwNi5w8d0lg7rG2NnL7eUoeNetn0przdaKXHUwMDE4tnLAze3a7T8uXFzYXHUwMDFjIn0= timevalue

    Run the following from the command prompt to preview them.

    textual easing\n

    You can specify which easing method to use via the easing parameter on the animate method. The default easing method is \"in_out_cubic\" which accelerates and then decelerates to produce a pleasing organic motion.

    Note

    The textual easing preview requires the textual-dev package to be installed (using pip install textual-dev).

    "},{"location":"guide/animation/#completion-callbacks","title":"Completion callbacks","text":"

    You can pass a callable to the animator via the on_complete parameter. Textual will run the callable when the animation has completed.

    "},{"location":"guide/animation/#delaying-animations","title":"Delaying animations","text":"

    You can delay the start of an animation with the delay parameter of the animate method. This parameter accepts a float value representing the number of seconds to delay the animation by. For example, self.box.styles.animate(\"opacity\", value=0.0, duration=2.0, delay=5.0) delays the start of the animation by five seconds, meaning the animation will start after 5 seconds and complete 2 seconds after that.

    "},{"location":"guide/app/","title":"App Basics","text":"

    In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters.

    "},{"location":"guide/app/#the-app-class","title":"The App class","text":"

    The first step in building a Textual app is to import the App class and create a subclass. Let's look at the simplest app class:

    from textual.app import App\n\n\nclass MyApp(App):\n    pass\n
    "},{"location":"guide/app/#the-run-method","title":"The run method","text":"

    To run an app we create an instance and call run().

    simple02.py
    from textual.app import App\n\n\nclass MyApp(App):\n    pass\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n

    Apps don't get much simpler than this\u2014don't expect it to do much.

    Tip

    The __name__ == \"__main__\": condition is true only if you run the file with python command. This allows us to import app without running the app immediately. It also allows the devtools run command to run the app in development mode. See the Python docs for more information.

    If we run this app with python simple02.py you will see a blank terminal, something like the following:

    MyApp

    When you call App.run() Textual puts the terminal in to a special state called application mode. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the screen).

    If you hit Ctrl+C Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored.

    Tip

    A side effect of application mode is that you may no longer be able to select and copy text in the usual way. Terminals typically offer a way to bypass this limit with a key modifier. On iTerm you can select text if you hold the Option key. See the documentation for your terminal software for how to select text in application mode.

    "},{"location":"guide/app/#events","title":"Events","text":"

    Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods prefixed with on_ followed by the name of the event.

    One such event is the mount event which is sent to an application after it enters application mode. You can respond to this event by defining a method called on_mount.

    Info

    You may have noticed we use the term \"send\" and \"sent\" in relation to event handler methods in preference to \"calling\". This is because Textual uses a message passing system where events are passed (or sent) between components. See events for details.

    Another such event is the key event which is sent when the user presses a key. The following example contains handlers for both those events:

    event01.py
    from textual.app import App\nfrom textual import events\n\n\nclass EventApp(App):\n\n    COLORS = [\n        \"white\",\n        \"maroon\",\n        \"red\",\n        \"purple\",\n        \"fuchsia\",\n        \"olive\",\n        \"yellow\",\n        \"navy\",\n        \"teal\",\n        \"aqua\",\n    ]\n\n    def on_mount(self) -> None:\n        self.screen.styles.background = \"darkblue\"\n\n    def on_key(self, event: events.Key) -> None:\n        if event.key.isdecimal():\n            self.screen.styles.background = self.COLORS[int(event.key)]\n\n\nif __name__ == \"__main__\":\n    app = EventApp()\n    app.run()\n

    The on_mount handler sets the self.screen.styles.background attribute to \"darkblue\" which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.

    EventApp

    The key event handler (on_key) has an event parameter which will receive a Key instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.

    Note

    It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.

    Some events contain additional information you can inspect in the handler. The Key event has a key attribute which is the name of the key that was pressed. The on_key method above uses this attribute to change the background color if any of the keys from 0 to 9 are pressed.

    "},{"location":"guide/app/#async-events","title":"Async events","text":"

    Textual is powered by Python's asyncio framework which uses the async and await keywords.

    Textual knows to await your event handlers if they are coroutines (i.e. prefixed with the async keyword). Regular functions are generally fine unless you plan on integrating other async libraries (such as httpx for reading data from the internet).

    Tip

    For a friendly introduction to async programming in Python, see FastAPI's concurrent burgers article.

    "},{"location":"guide/app/#widgets","title":"Widgets","text":"

    Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.

    Widgets can be as simple as a piece of text, a button, or a fully-fledged component like a text editor or file browser (which may contain widgets of their own).

    "},{"location":"guide/app/#composing","title":"Composing","text":"

    To add widgets to your app implement a compose() method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a generator.

    The following example imports a builtin Welcome widget and yields it from App.compose().

    widgets01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Welcome\n\n\nclass WelcomeApp(App):\n    def compose(self) -> ComposeResult:\n        yield Welcome()\n\n    def on_button_pressed(self) -> None:\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    When you run this code, Textual will mount the Welcome widget which contains Markdown content and a button:

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Notice the on_button_pressed method which handles the Button.Pressed event sent by a button contained in the Welcome widget. The handler calls App.exit() to exit the app.

    "},{"location":"guide/app/#mounting","title":"Mounting","text":"

    While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling mount() which will add a new widget to the UI.

    Here's an app which adds a welcome widget in response to any key press:

    widgets02.py
    from textual.app import App\nfrom textual.widgets import Welcome\n\n\nclass WelcomeApp(App):\n    def on_key(self) -> None:\n        self.mount(Welcome())\n\n    def on_button_pressed(self) -> None:\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503\u2582\u2582 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0 \u258c\u00a0that\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0 \u258c\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn \u258c\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 \u258c\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 OK \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#awaiting-mount","title":"Awaiting mount","text":"

    When you mount a widget, Textual will mount everything the widget composes. Textual guarantees that the mounting will be complete by the next message handler, but not immediately after the call to mount(). This may be a problem if you want to make any changes to the widget in the same message handler.

    Let's first illustrate the problem with an example. The following code will mount the Welcome widget in response to a key press. It will also attempt to modify the Button in the Welcome widget by changing its label from \"OK\" to \"YES!\".

    from textual.app import App\nfrom textual.widgets import Button, Welcome\n\n\nclass WelcomeApp(App):\n    def on_key(self) -> None:\n        self.mount(Welcome())\n        self.query_one(Button).label = \"YES!\" # (1)!\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n
    1. See queries for more information on the query_one method.

    If you run this example, you will find that Textual raises a NoMatches exception when you press a key. This is because the mount process has not yet completed when we attempt to change the button.

    To solve this we can optionally await the result of mount(), which requires we make the function async. This guarantees that by the following line, the Button has been mounted, and we can change its label.

    from textual.app import App\nfrom textual.widgets import Button, Welcome\n\n\nclass WelcomeApp(App):\n    async def on_key(self) -> None:\n        await self.mount(Welcome())\n        self.query_one(Button).label = \"YES!\"\n\n\nif __name__ == \"__main__\":\n    app = WelcomeApp()\n    app.run()\n

    Here's the output. Note the changed button text:

    WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YES! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#exiting","title":"Exiting","text":"

    An app will run until you call App.exit() which will exit application mode and the run method will return. If this is the last line in your code you will return to the command prompt.

    The exit method will also accept an optional positional value to be returned by run(). The following example uses this to return the id (identifier) of a clicked button.

    question01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    Running this app will give you the following:

    QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Yes \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 No \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Clicking either of those buttons will exit the app, and the run() method will return either \"yes\" or \"no\" depending on button clicked.

    "},{"location":"guide/app/#return-type","title":"Return type","text":"

    You may have noticed that we subclassed App[str] rather than the usual App.

    question01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    The addition of [str] tells mypy that run() is expected to return a string. It may also return None if App.exit() is called without a return value, so the return type of run will be str | None. Replace the str in [str] with the type of the value you intend to call the exit method with.

    Note

    Type annotations are entirely optional (but recommended) with Textual.

    "},{"location":"guide/app/#return-code","title":"Return code","text":"

    When you exit a Textual app with App.exit(), you can optionally specify a return code with the return_code parameter.

    What are return codes?

    Returns codes are a standard feature provided by your operating system. When any application exits it can return an integer to indicate if it was successful or not. A return code of 0 indicates success, any other value indicates that an error occurred. The exact meaning of a non-zero return code is application-dependant.

    When a Textual app exits normally, the return code will be 0. If there is an unhandled exception, Textual will set a return code of 1. You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception.

    Here's an example of setting a return code for an error condition:

    if critical_error:\n    self.exit(return_code=4, message=\"Critical error occurred\")\n

    The app's return code can be queried with app.return_code, which will be None if it hasn't been set, or an integer.

    Textual won't explicitly exit the process. To exit the app with a return code, you should call sys.exit. Here's how you might do that:

    if __name__ == \"__main__\"\n    app = MyApp()\n    app.run()\n    import sys\n    sys.exit(app.return_code or 0)\n
    "},{"location":"guide/app/#suspending","title":"Suspending","text":"

    A Textual app may be suspended so you can leave application mode for a period of time. This is often used to temporarily replace your app with another terminal application.

    You could use this to allow the user to edit content with their preferred text editor, for example.

    Info

    App suspension is unavailable with textual-web.

    "},{"location":"guide/app/#suspend-context-manager","title":"Suspend context manager","text":"

    You can use the App.suspend context manager to suspend your app. The following Textual app will launch vim (a text editor) when the user clicks a button:

    suspend.pyOutput
    from os import system\n\nfrom textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass SuspendingApp(App[None]):\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"Open the editor\", id=\"edit\")\n\n    @on(Button.Pressed, \"#edit\")\n    def run_external_editor(self) -> None:\n        with self.suspend():  # (1)!\n            system(\"vim\")\n\n\nif __name__ == \"__main__\":\n    SuspendingApp().run()\n
    1. All code in the body of the with statement will be run while the app is suspended.

    SuspendingApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Open\u00a0the\u00a0editor \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#suspending-from-foreground","title":"Suspending from foreground","text":"

    On Unix and Unix-like systems (GNU/Linux, macOS, etc) Textual has support for the user pressing a key combination to suspend the application as the foreground process. Ordinarily this key combination is Ctrl+Z; in a Textual application this is disabled by default, but an action is provided (action_suspend_process) that you can bind in the usual way. For example:

    suspend_process.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.binding import Binding\nfrom textual.widgets import Label\n\n\nclass SuspendKeysApp(App[None]):\n\n    BINDINGS = [Binding(\"ctrl+z\", \"suspend_process\")]\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Press Ctrl+Z to suspend!\")\n\n\nif __name__ == \"__main__\":\n    SuspendKeysApp().run()\n

    SuspendKeysApp Press\u00a0Ctrl+Z\u00a0to\u00a0suspend!

    Note

    If suspend_process is called on Windows, or when your application is being hosted under Textual Web, the call will be ignored.

    "},{"location":"guide/app/#css","title":"CSS","text":"

    Textual apps can reference CSS files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).

    Info

    Textual apps typically use the extension .tcss for external CSS files to differentiate them from browser (.css) files.

    The chapter on Textual CSS describes how to use CSS in detail. For now let's look at how your app references external CSS files.

    The following example enables loading of CSS by adding a CSS_PATH class variable:

    question02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Label\n\n\nclass QuestionApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n

    Note

    We also added an id to the Label, because we want to style it in the CSS.

    If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references \"question01.tcss\" in the same directory as the Python code. Here is that CSS file:

    question02.tcss
    Screen {\n    layout: grid;\n    grid-size: 2;\n    grid-gutter: 2;\n    padding: 2;\n}\n#question {\n    width: 100%;\n    height: 100%;\n    column-span: 2;\n    content-align: center bottom;\n    text-style: bold;\n}\n\nButton {\n    width: 100%;\n}\n

    When \"question02.py\" runs it will load \"question02.tcss\" and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:

    QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/app/#classvar-css","title":"Classvar CSS","text":"

    While external CSS files are recommended for most applications, and enable some cool features like live editing, you can also specify the CSS directly within the Python code.

    To do this set a CSS class variable on the app to a string containing your CSS.

    Here's the question app with classvar CSS:

    question03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\n\n\nclass QuestionApp(App[str]):\n    CSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n        grid-gutter: 2;\n        padding: 2;\n    }\n    #question {\n        width: 100%;\n        height: 100%;\n        column-span: 2;\n        content-align: center bottom;\n        text-style: bold;\n    }\n\n    Button {\n        width: 100%;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = QuestionApp()\n    reply = app.run()\n    print(reply)\n
    "},{"location":"guide/app/#title-and-subtitle","title":"Title and subtitle","text":"

    Textual apps have a title attribute which is typically the name of your application, and an optional sub_title attribute which adds additional context (such as the file your are working on). By default, title will be set to the name of your App class, and sub_title is empty. You can change these defaults by defining TITLE and SUB_TITLE class variables. Here's an example of that:

    question_title01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Header, Label\n\n\nclass MyApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n    TITLE = \"A Question App\"\n    SUB_TITLE = \"The most important question\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    reply = app.run()\n    print(reply)\n

    Note that the title and subtitle are displayed by the builtin Header widget at the top of the screen:

    A\u00a0Question\u00a0App \u2b58A\u00a0Question\u00a0App\u00a0\u2014\u00a0The\u00a0most\u00a0important\u00a0question Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    You can also set the title attributes dynamically within a method of your app. The following example sets the title and subtitle in response to a key press:

    question_title02.py
    from textual.app import App, ComposeResult\nfrom textual.events import Key\nfrom textual.widgets import Button, Header, Label\n\n\nclass MyApp(App[str]):\n    CSS_PATH = \"question02.tcss\"\n    TITLE = \"A Question App\"\n    SUB_TITLE = \"The most important question\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(\"Do you love Textual?\", id=\"question\")\n        yield Button(\"Yes\", id=\"yes\", variant=\"primary\")\n        yield Button(\"No\", id=\"no\", variant=\"error\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(event.button.id)\n\n    def on_key(self, event: Key):\n        self.title = event.key\n        self.sub_title = f\"You just pressed {event.key}!\"\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    reply = app.run()\n    print(reply)\n

    If you run this app and press the T key, you should see the header update accordingly:

    A\u00a0Question\u00a0App \u2b58t\u00a0\u2014\u00a0You\u00a0just\u00a0pressed\u00a0t! Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Info

    Note that there is no need to explicitly refresh the screen when setting the title attributes. This is an example of reactivity, which we will cover later in the guide.

    "},{"location":"guide/app/#whats-next","title":"What's next","text":"

    In the following chapter we will learn more about how to apply styles to your widgets and app.

    "},{"location":"guide/command_palette/","title":"Command Palette","text":"

    Textual apps have a built-in command palette, which gives users a quick way to access certain functionality within your app.

    In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands.

    "},{"location":"guide/command_palette/#launching-the-command-palette","title":"Launching the command palette","text":"

    Press Ctrl + \\ (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget. Textual will suggest commands as you type in that input. Press Up or Down to select a command from the list, and Enter to invoke it.

    Commands are looked up via a fuzzy search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command. For instance the \"Toggle light/dark mode\" command will be shown if you type \"to\" (for toggle), but you could also type \"dm\" (to match dark mode). This scheme allows the user to quickly get to a particular command with a minimum of key-presses.

    Command PaletteCommand Palette after 't'Command Palette after 'td'

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0eCommand\u00a0Palette\u00a0Search... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0et Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Ring\u00a0the\u00a0bell Ring\u00a0the\u00a0terminal's\u00a0'bell' \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0etd Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/command_palette/#default-commands","title":"Default commands","text":"

    Textual apps have the following commands enabled by default:

    • \"Toggle light/dark mode\" This will toggle between light and dark mode, by setting App.dark to either True or False.
    • \"Quit the application\" Quits the application. The equivalent of pressing ++ctrl+C++.
    • \"Play the bell\" Plays the terminal bell, by calling App.bell.
    "},{"location":"guide/command_palette/#command-providers","title":"Command providers","text":"

    To add your own command(s) to the command palette, define a command.Provider class then add it to the COMMANDS class var on your App class.

    Let's look at a simple example which adds the ability to open Python files via the command palette.

    The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it.

    Tip

    If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files.

    command01.py
    from __future__ import annotations\n\nfrom functools import partial\nfrom pathlib import Path\n\nfrom textual.app import App, ComposeResult\nfrom textual.command import Hit, Hits, Provider\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Static\n\n\nclass PythonFileCommands(Provider):\n    \"\"\"A command provider to open a Python file in the current working directory.\"\"\"\n\n    def read_files(self) -> list[Path]:\n        \"\"\"Get a list of Python files in the current working directory.\"\"\"\n        return list(Path(\"./\").glob(\"*.py\"))\n\n    async def startup(self) -> None:  # (1)!\n        \"\"\"Called once when the command palette is opened, prior to searching.\"\"\"\n        worker = self.app.run_worker(self.read_files, thread=True)\n        self.python_paths = await worker.wait()\n\n    async def search(self, query: str) -> Hits:  # (2)!\n        \"\"\"Search for Python files.\"\"\"\n        matcher = self.matcher(query)  # (3)!\n\n        app = self.app\n        assert isinstance(app, ViewerApp)\n\n        for path in self.python_paths:\n            command = f\"open {str(path)}\"\n            score = matcher.match(command)  # (4)!\n            if score > 0:\n                yield Hit(\n                    score,\n                    matcher.highlight(command),  # (5)!\n                    partial(app.open_file, path),\n                    help=\"Open this file in the viewer\",\n                )\n\n\nclass ViewerApp(App):\n    \"\"\"Demonstrate a command source.\"\"\"\n\n    COMMANDS = App.COMMANDS | {PythonFileCommands}  # (6)!\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            yield Static(id=\"code\", expand=True)\n\n    def open_file(self, path: Path) -> None:\n        \"\"\"Open and display a file with syntax highlighting.\"\"\"\n        from rich.syntax import Syntax\n\n        syntax = Syntax.from_path(\n            str(path),\n            line_numbers=True,\n            word_wrap=False,\n            indent_guides=True,\n            theme=\"github-dark\",\n        )\n        self.query_one(\"#code\", Static).update(syntax)\n\n\nif __name__ == \"__main__\":\n    app = ViewerApp()\n    app.run()\n
    1. This method is called when the command palette is first opened.
    2. Called on each key-press.
    3. Get a Matcher instance to compare against hits.
    4. Use the matcher to get a score.
    5. Highlights matching letters in the search.
    6. Adds our custom command provider and the default command provider.

    There are three methods you can override in a command provider: startup, search, and shutdown. All of these methods should be coroutines (async def). Only search is required, the other methods are optional. Let's explore those methods in detail.

    "},{"location":"guide/command_palette/#startup-method","title":"startup method","text":"

    The startup method is called when the command palette is opened. You can use this method as way of performing work that needs to be done prior to searching. In the example, we use this method to get the Python (.py) files in the current working directory.

    "},{"location":"guide/command_palette/#search-method","title":"search method","text":"

    The search method is responsible for finding results (or hits) that match the user's input. This method should yield Hit objects for any command that matches the query argument.

    Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling matcher. This object has a match() method which compares the user's search term against the potential command and returns a score. A score of zero means no hit, and you can discard the potential command. A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match.

    The Hit contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string. It also contains a callback, which will be run if the user selects that command.

    In the example above, the callback is a lambda which calls the open_file method in the example app.

    Note

    Unlike most other places in Textual, errors in command provider will not exit the app. This is a deliberate design decision taken to prevent a single broken Provider class from making the command palette unusable. Errors in command providers will be logged to the console.

    "},{"location":"guide/command_palette/#shutdown-method","title":"Shutdown method","text":"

    The shutdown method is called when the command palette is closed. You can use this as a hook to gracefully close any objects you created in startup.

    "},{"location":"guide/command_palette/#screen-commands","title":"Screen commands","text":"

    You can also associate commands with a screen by adding a COMMANDS class var to your Screen class.

    Commands defined on a screen are only considered when that screen is active. You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app.

    "},{"location":"guide/command_palette/#disabling-the-command-palette","title":"Disabling the command palette","text":"

    The command palette is enabled by default. If you would prefer not to have the command palette, you can set ENABLE_COMMAND_PALETTE = False on your app class.

    Here's an app class with no command palette:

    class NoPaletteApp(App):\n    ENABLE_COMMAND_PALETTE = False\n
    "},{"location":"guide/design/","title":"Design System","text":"

    Textual's design system consists of a number of predefined colors and guidelines for how to use them in your app.

    You don't have to follow these guidelines, but if you do, you will be able to mix builtin widgets with third party widgets and your own creations, without worrying about clashing colors.

    Information

    Textual's color system is based on Google's Material design system, modified to suit the terminal.

    "},{"location":"guide/design/#designing-with-colors","title":"Designing with Colors","text":"

    Textual pre-defines a number of colors as CSS variables. For instance, the CSS variable $primary is set to #004578 (the blue used in headers). You can use $primary in place of the color in the background and color rules, or other any other rule that accepts a color.

    Here's an example of CSS that uses color variables:

    MyWidget {\n    background: $primary;\n    color: $text;\n}\n

    Using variables rather than explicit colors allows Textual to apply color themes. Textual supplies a default light and dark theme, but in the future many more themes will be available.

    "},{"location":"guide/design/#base-colors","title":"Base Colors","text":"

    There are 12 base colors defined in the color scheme. The following table lists each of the color names (as used in CSS) and a description of where to use them.

    Color Description $primary The primary color, can be considered the branding color. Typically used for titles, and backgrounds for strong emphasis. $secondary An alternative branding color, used for similar purposes as $primary, where an app needs to differentiate something from the primary color. $primary-background The primary color applied to a background. On light mode this is the same as $primary. In dark mode this is a dimmed version of $primary. $secondary-background The secondary color applied to a background. On light mode this is the same as $secondary. In dark mode this is a dimmed version of $secondary. $background A color used for the background, where there is no content. $surface The color underneath text. $panel A color used to differentiate a part of the UI form the main content. Typically used for dialogs or sidebars. $boost A color with alpha that can be used to create layers on a background. $warning Indicates a warning. Text or background. $error Indicates an error. Text or background. $success Used to indicate success. Text or background. $accent Used sparingly to draw attention to a part of the UI (typically borders around focused widgets)."},{"location":"guide/design/#shades","title":"Shades","text":"

    For every color, Textual generates 3 dark shades and 3 light shades.

    • Add -lighten-1, -lighten-2, or -lighten-3 to the color's variable name to get lighter shades (3 is the lightest).
    • Add -darken-1, -darken-2, and -darken-3 to a color to get the darker shades (3 is the darkest).

    For example, $secondary-darken-1 is a slightly darkened $secondary, and $error-lighten-3 is a very light version of the $error color.

    "},{"location":"guide/design/#dark-mode","title":"Dark mode","text":"

    There are two color themes in Textual, a light mode and dark mode. You can switch between them by toggling the dark attribute on the App class.

    In dark mode $background and $surface are off-black. Dark mode also set $primary-background and $secondary-background to dark versions of $primary and $secondary.

    "},{"location":"guide/design/#text-color","title":"Text color","text":"

    The design system defines three CSS variables you should use for text color.

    • $text sets the color of text in your app. Most text in your app should have this color.
    • $text-muted sets a slightly faded text color. Use this for text which has lower importance. For instance a sub-title or supplementary information.
    • $text-disabled sets faded out text which indicates it has been disabled. For instance, menu items which are not applicable and can't be clicked.

    You can set these colors via the color property. The design system uses auto colors for text, which means that Textual will pick either white or black (whichever has better contrast).

    Information

    These text colors all have some alpha applied, so that even $text isn't pure white or pure black. This is done because blending in a little of the background color produces text that is not so harsh on the eyes.

    "},{"location":"guide/design/#theming","title":"Theming","text":"

    In a future version of Textual you will be able to modify theme colors directly, and allow users to configure preferred themes.

    "},{"location":"guide/design/#color-preview","title":"Color Preview","text":"

    Run the following from the command line to preview the colors defined in the color system:

    textual colors\n
    "},{"location":"guide/design/#theme-reference","title":"Theme Reference","text":"

    Here's a list of the colors defined in the default light and dark themes.

    Note

    $boost will look different on different backgrounds because of its alpha channel.

    Textual\u00a0Theme\u00a0Colors \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Light\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

    "},{"location":"guide/devtools/","title":"Devtools","text":"

    Note

    If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.

    See getting started for details.

    Textual comes with a command line application of the same name. The textual command is a super useful tool that will help you to build apps.

    Take a moment to look through the available subcommands. There will be even more helpful tools here in the future.

    textual --help\n
    "},{"location":"guide/devtools/#run","title":"Run","text":"

    The run sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app.

    textual run my_app.py\n

    This is equivalent to running python my_app.py from the command prompt, but will allow you to set various switches which can help you debug, such as --dev which enable the Console.

    See the run subcommand's help for details:

    textual run --help\n

    You can also run Textual apps from a python import. The following command would import music.play and run a Textual app in that module:

    textual run music.play\n

    This assumes you have a Textual app instance called app in music.play. If your app has a different name, you can append it after a colon:

    textual run music.play:MusicPlayerApp\n

    Note

    This works for both Textual app instances and classes.

    "},{"location":"guide/devtools/#running-from-commands","title":"Running from commands","text":"

    If your app is installed as a command line script, you can use the -c switch to run it. For instance, the following will run the textual colors command:

    textual run -c textual colors\n
    "},{"location":"guide/devtools/#live-editing","title":"Live editing","text":"

    If you combine the run command with the --dev switch your app will run in development mode.

    textual run --dev my_app.py\n

    One of the features of dev mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.

    This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.

    "},{"location":"guide/devtools/#console","title":"Console","text":"

    When building a typical terminal application you are generally unable to use print when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores print and adds a few additional features to help you debug.

    To use the console, open up two terminal emulators. Run the following in one of the terminals:

    textual console\n

    You should see the Textual devtools welcome message:

    textual\u00a0console \u258cTextual\u00a0Development\u00a0Console\u00a0v0.44.1 \u258cRun\u00a0a\u00a0Textual\u00a0app\u00a0with\u00a0textual\u00a0run\u00a0--dev\u00a0my_app.py\u00a0to\u00a0connect. \u258cPress\u00a0Ctrl+C\u00a0to\u00a0quit.

    In the other console, run your application with textual run and the --dev switch:

    textual run --dev my_app.py\n

    Anything you print from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application.

    "},{"location":"guide/devtools/#increasing-verbosity","title":"Increasing verbosity","text":"

    Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as \"verbose\" and will be excluded from the logs. If you want to see these log messages, you can add the -v switch.

    textual console -v\n
    "},{"location":"guide/devtools/#decreasing-verbosity","title":"Decreasing verbosity","text":"

    Log messages are classififed in to groups, and the -x flag can be used to exclude all message from a group. The groups are: EVENT, DEBUG, INFO, WARNING, ERROR, PRINT, SYSTEM, and LOGGING. The group a message belongs to is printed after its timestamp.

    Multiple groups may be excluded, for example to exclude everything except warning, errors, and print statements:

    textual console -x SYSTEM -x EVENT -x DEBUG -x INFO\n
    "},{"location":"guide/devtools/#custom-port","title":"Custom port","text":"

    You can use the option --port to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default:

    textual console --port 7342\n

    Then, use the command run with the same --port option:

    textual run --dev --port 7342 my_app.py\n
    "},{"location":"guide/devtools/#textual-log","title":"Textual log","text":"

    Use the log function to pretty-print data structures and anything that Rich can display.

    You can import the log function as follows:

    from textual import log\n

    Here's a few examples of writing to the console, with log:

    def on_mount(self) -> None:\n    log(\"Hello, World\")  # simple string\n    log(locals())  # Log local variables\n    log(children=self.children, pi=3.141592)  # key/values\n    log(self.tree)  # Rich renderables\n
    "},{"location":"guide/devtools/#log-method","title":"Log method","text":"

    There's a convenient shortcut to log on the App and Widget objects. This is useful in event handlers. Here's an example:

    from textual.app import App\n\nclass LogApp(App):\n\n    def on_load(self):\n        self.log(\"In the log handler!\", pi=3.141529)\n\n    def on_mount(self):\n        self.log(self.tree)\n\nif __name__ == \"__main__\":\n    LogApp().run()\n
    "},{"location":"guide/devtools/#logging-handler","title":"Logging handler","text":"

    Textual has a logging handler which will write anything logged via the builtin logging library to the devtools. This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs.

    Note

    The logging library works with strings only, so you won't be able to log Rich renderables such as self.tree with the logging handler.

    Here's an example of configuring logging to use the TextualHandler.

    import logging\nfrom textual.app import App\nfrom textual.logging import TextualHandler\n\nlogging.basicConfig(\n    level=\"NOTSET\",\n    handlers=[TextualHandler()],\n)\n\n\nclass LogApp(App):\n    \"\"\"Using logging with Textual.\"\"\"\n\n    def on_mount(self) -> None:\n        logging.debug(\"Logged via TextualHandler\")\n\n\nif __name__ == \"__main__\":\n    LogApp().run()\n
    "},{"location":"guide/events/","title":"Events and Messages","text":"

    We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.

    "},{"location":"guide/events/#messages","title":"Messages","text":"

    Events are a particular kind of message sent by Textual in response to input and other state changes. Events are reserved for use by Textual, but you can also create custom messages for the purpose of coordinating between widgets in your app.

    More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.

    "},{"location":"guide/events/#message-queue","title":"Message Queue","text":"

    Every App and Widget object contains a message queue. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line.

    Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right away.

    This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive.

    Tip

    The FastAPI docs have an excellent introduction to Python async programming.

    By way of an example, let's consider what happens if you were to type \"Text\" in to a Input widget. When you hit the T key, Textual creates a key event and sends it to the widget's message queue. Ditto for E, X, and T.

    The widget's task will pick the first message from the queue (a key event for the T key) and call the on_key method with the event as the first argument. In other words it will call Input.on_key(event), which updates the display to show the new letter.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9zq+g2C97q4Iyj57XVm3dXG6Eh8NcdTAwMWJCSPbuXHUwMDE2JWzZViw/sGRcZknlv99cdTAwMWVcdTAwMDGWLEu2McaYutdcdFx1MDAxOEujUVvTp/ucnpF+rqyurkV3XHUwMDFkb+2P1TXvtuxcdTAwMDZ+pev2197Z7TdeN/TbLdzF4s9hu9ctxy3rUdRcdP94/77pdlx1MDAxYl7UXHTcsufc+GHPXHLCqFfx20653XzvR14z/Lf9feg2vT877WYl6jrJSda9ilx1MDAxZrW79+fyXHUwMDAyr+m1olx1MDAxMHv/XHUwMDBmfl5d/Vx1MDAxOf9OWVx1MDAxN/gtL25cdTAwMWJvTWzjhGW3XHUwMDFltluxnYxpJYFcbjlo4IdcdTAwMWbxTJFXwb1VtNZL9thNa3ew7a9v1GuNw6P92uXn453d/r5Mzlr1g+Asulx1MDAwYu4vgluu97opm8Ko2254XHUwMDE3fiWqP16z1PbBcVx1MDAxNTeso1x1MDAwMYPd3XavVm95of3udLC13XHLfnRnt1x1MDAxMTLY6rZqcSfJllv8XHUwMDA0TDlcXFFiuFx1MDAxMoNcdTAwMWT2UC7AoZRRZncxo1x1MDAxNM9cdTAwMTi12Vx1MDAwZXBcdTAwMDTQqN9I/EqsunLLjVx1MDAxYZrWqlxm2kRdt1x1MDAxNXbcLo5T0q7/+HVcdTAwMWRJQSpcbmBwREApXHUwMDE4NKl7fq1cdTAwMWVZczRzXHUwMDE4lZRcbs2pXHUwMDAyloxL6MVDXCK0XHUwMDA2LiE51FrQKVVit/gne0HrbrfzcN3WYktT1tuPWymfSlx1MDAwZe51Ku69XHUwMDAzUCk5XHUwMDEzaCmXLLmg6GdcctzZ6lx1MDAwNUGyrV1u5PhMXHUwMDE4ud1ow29V/FYte4jXqlx1MDAxNOxcdNww2mw3m36EZlx1MDAxY7f9VpRtXHUwMDEx9/uh2233655byem5cF/HdpeAyL6Sv1ZcdTAwMTO3iT9cZv7+511u6+Ihta+RwUy6W0m//3r3RDxLkt36iGdcdTAwMDVUXHUwMDEyTVJccibhueyT7cMvnXWz8dm/8b5cdTAwMDU7p+ftT0uPZ0Ul4llcdTAwMWJcIllcdTAwMTbP3MGQxlx0XHUwMDA3/Mf0y8HZOIRxjfiQglBcbjRcdTAwMGbNyjhMMcE5XHUwMDA1wVjyVVx1MDAxZsBMNTWMS6lcdTAwMTKY/1x1MDAxZs6vXGLnwiG1r+xgPlx1MDAxMcxdr1x1MDAxY937clx1MDAwZaKFVEWIplxuXHUwMDA3jFx1MDAwYlx1MDAwNXpqSJ9/b1xis8v5XHUwMDA1bFx1MDAxZVx1MDAxZV1Hd5dHSjZngzTNuuDjcWFcdTAwMWIpylxcMzRwR2mBVCSDaClExognYfg3KEuvKlx1MDAwMHJcdTAwMTKyTK7pXHUwMDAwszpcdTAwMTXFXHUwMDFlUMqBMMFcdTAwMDQkXHUwMDA2z1xypVx1MDAwMyf6mfK0gc9E3m1cdTAwMTJ4Ulx1MDAwM1xcPWq4ncjb0Xe1T0dbknxcdTAwMDH6w19cdTAwMWK0+/Uuv9v7gzfV0Y3r9Xav3fN+dHHEXHUwMDBmw8Nob/gsj+d3LepS/T46+lx1MDAxY0PLWMxcZn3/NFxcaCFcXDDVcmGQ1k6NlvyLufRo0Vx1MDAwNWjR4DxcdTAwMGIv4ymsyEFcZuNZxFx1MDAwMKXKMMlnSGuh/bDotFZtt6Iz/0csiMjQ1m236Vx1MDAwNzGvXHUwMDE4bI6d0mrBXHUwMDFia5Oz59393vDu/vx77fPfa/9KX9rQi1x0XHUwMDFj2meGXHUwMDBl/lx1MDAxMPi1Vky9sFx1MDAwM6875OCRj9pv0KDpVyrphFFGi1xc7LNbmibOt7t+zW+5weexXHUwMDA2z560kPNcdTAwMTYmLcGUXCKYJNnUMCTVjdb2xkWTXHUwMDFk7PbPvm9dnVx1MDAxZJWbfPlhSFx1MDAxZKo5Q6bJjaaSXHUwMDBmYVFcYuEg61dcdTAwMDb5qFx1MDAwNinUc3A5hzymKCVcblxy0U+H5Wx57H5kw89cdTAwMDcnXHUwMDFmLjpy0z24U7J1XHUwMDFhfe3fVvNcdTAwMTNOjK2JeWxSesw/4fKlMfSeQlx1MDAwMCHTXHUwMDA0LZWeXsiNv8xLXHUwMDBiIDlcdTAwMGVAmMzMvFx1MDAwMDSPxEa1llx1MDAwNNW+nIFcbr7hzOYtOrNNSFx1MDAwNlx1MDAxMzObNzGz3VPbXHUwMDFjUFwiqSpcdTAwMDIlXHUwMDAwXHUwMDAzxSVMXHLJ8VR7SSEpNHU4IZwrKrVG2TNcdTAwMDRJhTnN4GZNWTHNxGtULavx6azqoqRiUo5iXHUwMDExUypcdTAwMDDmKq2I1Pa/XHUwMDE4hSaAY00kWnMlXHUwMDA1g5HSikBcdTAwMWLwXHUwMDFi0DdaWUny3WPdf1x1MDAxYcpcdTAwMTdDu9yzVq5cdTAwMTNcdTAwMDdcdTAwMDNcdTAwMTWOlK3uXHUwMDBiW1xiXHUwMDAzwVPtam4njmeDwXzYNci5w/WcXCJ7dvrkR61xcvDtU/PM6NBcdTAwMWNdnlx1MDAxZbM8e9BcdTAwMWPCLP1AUqRMbFx1MDAxNchcdTAwMTF7XHUwMDE4sVU9Jlx1MDAwNGZcdTAwMDKQXHUwMDFj4/6IWXMuJmVcdTAwMDPBXFzrScWebF8jPpx0t5J+n4mcayOzW1x1MDAxM26BmVRSdIqpXHUwMDAz2aHZuNBcdTAwMDY2TrpcdTAwMDem8uV6//r6Q+922Vx1MDAwM1x1MDAxOVx1MDAwMHFQgiB70KhGbVx1MDAxMW0okiFcdTAwMWVcdTAwMWNFiFx1MDAxMdxcdTAwMDBcdTAwMGVcdTAwMDFJz7W8XHUwMDBlPVx1MDAxN1x1MDAxOHpcdTAwMTFcdTAwMWJcdTAwMGIrM92Prdzp175931xc3708//bps/6q6mqDPYeev1C3k1h//lx0p7T2W2f/ekdvVIOvXHUwMDE3f7ntvV23tHnWfltFMVxyxTVkQe0sr2HTU5fxw7e0iFx1MDAxN2NcdTAwMTGvucPmhvh56Fx0rVx1MDAwNDpH+ov9L8iJ20XLiVx06WuinLidKCdcbivVKchlQGlcYqXUwPRcdTAwMDJfbrPaycc72DpcdTAwMTW9nql978nLzt2yQ1JcdTAwMTnioGRcdTAwMWWpU1x1MDAwYtxupFYvNbeD3DdcdTAwMDeAXCJcdTAwMGJANEzb4MhcdTAwMTZcXFx1MDAxM1uufPNcdTAwMTR80yGIWlx1MDAwM1x1MDAwZvB6uDVv9brn9bx8XFzroYNcdTAwMDawXHK8ajRcdTAwMDbVUbtTXHUwMDA06aEvk8XvsEFjcVtYXHUwMDA2oHRMOqXcIJtcdTAwMTdPwO748V5S7GqgXHUwMDBlXHUwMDAwXHUwMDE4SbmmXFyZXGaCNThcZlx1MDAxOH1BXGZT7lCFSoZcYsZcdTAwMDQjyYBcZlx1MDAxMG2UwzhcdTAwMDGJTYhSSo+sl1x1MDAwMkzzgmj2XHUwMDAypPo161x1MDAwMONzwWq2XHUwMDBlgNFXgWGUaaF5stZg9VF3S0ehKJSzVlx1MDAwMcbn12FrcDhccjNKcCEwL+SWXHUwMDAwXGJcdTAwMDAj2MagXHUwMDFlXHUwMDAypkdsekslgPVCJ473jvhv0t9K+r0oflV8t9lOe2lqdkFcdTAwMTSuXHUwMDEyY4Rb5snp9Ms+x1x1MDAxN3qWNIBh8HK0XHUwMDE0dnEnXHUwMDE1SpiMXHUwMDFlwKjgMGUwglx1MDAwMTJcdTAwMTHgJmPY0+LYfUEzt1x1MDAwMKByuFxiZcaRoCG9IPUhZiFOXHUwMDE5oF2zyILnkJJnrFxmeZff7yTNLi5PXCLYgNOrvd3u7dFx+aJeOjmeVrP/pc7Pt672zlx1MDAwZvC6d3+cd8PN2+rB/DiUVjzh7i+k2XnxQlx1MDAxNiaBaOtPU0M0/2IuPUTNWIgqjvKBXHUwMDAzkZZoPFx1MDAxN6LjanR5cmF0/lx1MDAwZrRWXHUwMDEyg/lbWX89k2Bvty5R9/5cdTAwMWXL4MVcbvVcdFkmS/SHXHKdXHSBjFx1MDAxNM/BI/9QWpjpk+TJlemTm6PNXHUwMDEyLdPti7vatquDcNlcdTAwMTGokIMgK2RcXFKquExcdTAwMTWm4/k+TVx1MDAxZGOsXG5cdTAwMDDQ3OiXypGU58zyjep1ZVxiVUy/XHUwMDAw/F4x0yxUrW9ZsKzW3Vx1MDAxNuKwm1x1MDAwZu7FqvVhg8aCuFCtKz6O6zKM2IRNL9bHXHUwMDBm97LCWHNcdTAwMDcpP1hcdTAwMWOPwlhcdTAwMTPqXGKhha1+XHUwMDEzolPMZr4wNlxmo1x1MDAwNWpNlN2a4agksXNcdTAwMDBqQVx1MDAxZFwiXHUwMDA0xWCj8Vx1MDAwN1LceLDIRtgpU/1cdTAwMDIludeU7OOTw2p6qlxcoTI2wOyEoFxyeCzV6GHeXHUwMDFlXHUwMDFjylx1MDAxNzNtz/GyKM5cdGpx1KlKiFFjhMM1XHUwMDAwupZUWinJzYhRb0qxXHUwMDE3+rB9jXhv0t1K+n2mSXtKivVcdTAwMDBcdTAwMDVcdTAwMTTtXHUwMDA0zZk+jqldz5elXm1cdTAwMDM2LkNZ+Ytz9/Ro2eNcdTAwMThyfMdII6XAK0+0XHUwMDFjjmNcXChcdTAwMDe4jitIhEry2ktqhTBcdTAwMDQlXHUwMDAxWfD0Qalx2bioXHUwMDA2/CvtnqhcdTAwMGa3lZ0621x1MDAwZZ4/Z/9Wup1UVsg/4YuSsrGgL1x1MDAxMlx1MDAxZkZcdTAwMTXSXHUwMDE2VJqc2rtKp7+RZfxlXla4XHUwMDAzjIO7lo6ZXHUwMDE33OcxYc+IXHUwMDE0KJhmwftcdTAwMWKesI9cdTAwMTY9YT8hc02csI+ed2eLLr6zhVx1MDAxOUzDVlx1MDAwZk9ccstcdTAwMDNSql5cdTAwMWNdRKWQ7ZQvvG+eOP9SKoBludtcdTAwMGXD9bpcdTAwMWKV669cdTAwMGZNpHNcdTAwMGUyPs24Xb+YWmJcdTAwMWJ7XHJoh1x1MDAxMW0pIVx1MDAxNUSl65TzhqZQXGbZruFcdTAwMDaZJSbr1PrsoVx1MDAxYq1cdMdcdTAwMTbI2URcXLdcdTAwMTlcdTAwMDUuoFx1MDAwZeTmXHUwMDE1bkmjT1x1MDAwMO7sPitk4UM+7O3wXHUwMDFhuez0qaTSb7tl3W5tXHUwMDA3VyGr1Fp998dOUSrJ+N2Uj1x1MDAwNHhcdTAwMTFvxcG3t0pcdTAwMWJOhcy6q33+XHUwMDA3XHUwMDExhkrQRKpcdTAwMTdcXPolXHUwMDE0cVx1MDAwMKhRQJhGRZ6TVzhx7FJoSbhBXHUwMDExh/pqhFcqXHUwMDAzXHUwMDAyMM+8wsKwubmrXHUwMDE3XHUwMDA0fifMd1ZerHNcYlx1MDAxN1ZdmelXJ1+VTndcci25n8pcdTAwMDErl1x1MDAwM6M/qkZvXHUwMDE2b11gbMWAhY5cbkQwpDU8VVa9VzlcdTAwMWElNXqytM8jXHUwMDAwMY9qTd5cdTAwMTIphyiD0TteXHUwMDFmQSCnVkPtgklbXHUwMDE2tus8XHUwMDE4KtBRXHUwMDA1JCVSXCLD2VxmJdk34ammcH5AKo5xRD1hXHUwMDEyvXlrbrauv3/8us+jq1x1MDAxYrfqXHUwMDFk7+1cdTAwMTXd47okjootlIP5X2AsI4qAyD5uRWEoI1xc2ck5JVKqeTZPvVwiRLyQp1x1MDAwMrpcdTAwMGXG2te4dW9cdTAwMTGOKkFcdTAwMTQ5KqPx04fQW6f21HOXlEo/tpplJk5O9XW9XFzz5e6yeyrHXHUwMDFjT1x1MDAxOUFMYlx1MDAwZeFcIsNcdTAwMDCEdOzDkYjUklx1MDAxMmDPm8ii7ErrvHvX5lx1MDAxMlNcdTAwMDHiqDpLXHUwMDA1fGlcXHX8o1x1MDAwM3Rx4UNrJGmM8enDqiq3dvfDm1x1MDAxZF1vymr3MvhQqvpmtsLH4vgqRUnlUKGZNMo+XHUwMDA3avg2S8GIozVFSYNcdTAwMTRcdTAwMDFkXHUwMDE2RfNjq9xcdTAwMTBHWSdcdTAwMTSA0VvTvFx0XHUwMDFi7iBcdTAwMTNcdTAwMDVqn+oh7TrQrL9yaiTmhtd43Fx1MDAwN1+Iu45Zxc9cdTAwMDE4t3Cd2lt31zuNnb1bV1x1MDAxZl/pfr9zXHUwMDFjnFx1MDAxZpr9ZVx1MDAwZq2KO0rZKVx1MDAxMeuvOlWHi52VY9RcdTAwMDP0JFx1MDAwZZpcdTAwMTIqnuWtXHUwMDBmRfmc0KpcdTAwMWNcdTAwMTR3qGONQHVl1ySPuirTjp2HQkuJoExcdTAwMTgyqqwk2FuDXHUwMDA0vEJsfbqzrjxcdTAwMTSp19xO5yzCLtdcdTAwMWXn9NBqv/JQ2Uu6Wbvxvf5GXHUwMDFls4pftiRcdTAwMTZcdTAwMDPAeplnbf75a+XXf1x1MDAwMfpa2G0ifQ== events.Key(key=\"T\")events.Key(key=\"e\")events.Key(key=\"x\")Message queueon_key(event)Event handlerevents.Key(key=\"t\")

    When the on_key method returns, Textual will get the next event from the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an idle state.

    Note

    This example illustrates a point, but a typical app will be fast enough to have processed a key before the next event arrives. So it is unlikely you will have so many key events in the message queue.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPbOLb93r9cIpX+8l5Vi1xy3Fx1MDAwYlxcXHUwMDAwUzX1yrvlxI5cdTAwMTfF25splyxRiy1LskSvXf3f50JxLGohJVlL5ExUldgmaVx1MDAxMCbP3Vx1MDAwZi7++u3Dh4/RUzP8+I9cdTAwMGZcdTAwMWbDx0K+Vi228lx1MDAwZlx1MDAxZv/wx+/DVrvaqPMp6Pzcbty1XG6dKytR1Gz/488/b/Kt6zBq1vKFMLivtu/ytXZ0V6w2gkLj5s9qXHUwMDE03rT/z/+/l79cdP/ZbNxcdTAwMTSjVtC9SSYsVqNG69u9wlp4XHUwMDEz1qM2j/7//POHXHUwMDBmf3X+j82uVq2HnWs7R7tzU6D7j+416p15XCKR4tOye0G1vc53isJcIp8t8WzD7lx1MDAxOX/oY+FY1pu721x1MDAwN42709rGTqV1+bTXyHTvWqrWakfRU+3bQ8hcdTAwMTcqd63YnNpRq3FcdTAwMWSeVItR5fszi1x1MDAxZH/9vWK+XeFcdLyebjXuypV62PZ/u3w92mjmXHUwMDBi1ejJXHUwMDFmXHUwMDEz4vVovl7uXGbSPfLIP2VcdTAwMDBFYFx1MDAxZFx1MDAxOaW1QjSWXk/7XHUwMDAxMnxSKyc0WWONUYawb25rjVx1MDAxYb9cYp7b76Lz6U7uMl+4LvNcZuvF12uiVr7ebuZb/Lq61z18/6tcdTAwMDOSioxUyqFcdTAwMDDFd3u9pFx1MDAxMlbLlci/XHUwMDE2XHUwMDBiXHUwMDAxSJJSW5RGQXe27bDzZow2xlxup8zrXHQ/hWa22IHHv/tcdTAwMWZsJd9qvjy/j52pxqbvf9yIYav7y3fNYv5cdTAwMWJcdTAwMTAkXHUwMDExgjZcdTAwMWFcdTAwMTGlfT3PeLvmk/W7Wq17rFG4XHUwMDFlgp12lG9Fq9V6sVov9/9KWC8mnKnl29Fa4+amXHUwMDFh8TT2XHUwMDFi1XrUf0Vn3JVWq/FQXHTzxSEjJ55r+uG6wuQ/3e8+dOHT+eH1+3//MfTq5HfqP1x1MDAwM2+zO9xv8a9//zGZXFzHxbZPrqU1XHUwMDEy/VvrXHUwMDAyZJRgXHUwMDE3XHUwMDFmt+uHNbm/cqLXilLKp1P8urX0gk1cdTAwMTiAQaeVllx1MDAwNqxm+e5cdTAwMTVsXHUwMDE5KFx1MDAwM1JoaYGUk6J/arOTa1xmLFx0x4IpXHUwMDE0KiMsdoW2K9fGXHUwMDA1pNFJLdFcYsDYbL7LtZaSQHfVzy+5/oFynfxOO2f73+aEct1cblx1MDAwYtE3VFx1MDAwZlx1MDAxMW5cdTAwMGKq/+h34TYgyFkruiBcdTAwMTkl23pcdTAwMTez6vhuf0tcdTAwMWWXXHUwMDBln45WN6urXHUwMDBme2+TbdmPwe+/126w0zJbmy1VQGCsklpb/qe74uJHXHUwMDAwZ1x1MDAwMudcdTAwMDRcdTAwMTFYh85p0zeziUT7d1WgsKSVXHUwMDFhYrCpK1x1MDAxYa+SbFx1MDAwN0TXXHUwMDE5XCJWMNrNXnRfcfVXXGZ9L6/2+jS7cnZcdTAwMWGFZ1x1MDAwZW9Xwp2zbPSw1vxcdTAwMTiH6SveovAx+vh64u8/foZhe67+Y9xcdTAwMWJ2h/0uqDNUjqky3zPPmLhcdTAwMWIjksRdXHUwMDFhUsiuKcmx5T39MS+tvFx1MDAwYpcm71xiJlCzkvd0XHUwMDE3XVx1MDAwZpF4wH6JZ/3EXHUwMDEzVSrmc4wt8m3/w6KtdalRj46qz/7Rg+g5upm/qdY6XHUwMDBm+fVwXHUwMDA3qT7mvfdzXG4+hU//c1x1MDAxZD79819cdTAwMWbDf3383/izbYdcdTAwMWRcdTAwMDeV5+d6fnmlVi3XOyEjXHUwMDBmXHUwMDEwtnpQXHUwMDFmVTnGfb3gplosxq1ggWeU5zFb2XGsV6NVLVfr+VoudcJvt8RSOpckmyDY9+T34cZcdTAwMGagm0BQyX6y95VTKp5+NjuGtnaWXzalXHUwMDBljJLkUKFjV1x1MDAwN/pssVCBs8ZZJVxmsG+ippHN6W0xsVtG1mo5uWBOY4tLXHUwMDE2Tyh7sEVCXHUwMDFlZTZcdTAwMGLbUT17tT290fwvXHUwMDFmdpSJXHUwMDFmfsPlM/FSxtI1/TZeXHUwMDE44jBC4vg+ffpzXl49XCJS9VxiqkDNSo/MxMZrhWTi7+W/wcQ/LtrEjzCKI03845QmXHUwMDFlY2++XzSVcFx1MDAxY+lrM777fWLv7k5LVXyqf3Lbd+XqMUVN/Vx1MDAwZURTXHUwMDA11pJBpYXSZGLa6ttcYi4wXHUwMDE2vOdttXFcdTAwMTLtNLI5vY2X6KQlqWBcdTAwMGU58DT7tpYt3lZutrPQus3kSoXyVrNcdTAwMDJcdTAwMGbTm81fw85j2FG+w/BcdTAwMWIuoe9cdTAwMDAmMVx1MDAxZFxiXHUwMDAyyLDcqq5uXHUwMDFlpaDSn/PSKijWQGlcblxuXVx1MDAwMDNTULNwXHUwMDFlkDg45Hm8ISX4jp2HaNHOw1xiczvSeYimdFx1MDAxZYSEJNmUvqpsSajx81x1MDAwM6JcXCg+fd543nzOrZ6a1np0YrYuXHUwMDEzZLPQarTbmUo+KlQmrsXNXFw+Ob5cdKRcdTAwMDGSTrGEOid7xVNcdTAwMDdWXHUwMDBiw+60clx1MDAwNlx1MDAwNcr5pe/Yq1xiWFx1MDAxMtBnTaWMXHUwMDE1Sbt1OFx1MDAxMoFlh0YpQuzUXHUwMDA2+0WXWMU4K/Fcclx1MDAxOYRpRJescjSB6L5cdTAwMWS1XHUwMDA2XHUwMDEyXHUwMDEzzmA58lwimKB2/PDl7Ph+t+lobbN+vXffzl2VaDdcdTAwMDGzfbj7cWjFwFxuzViUgpW0xD6wXHUwMDAyaTYhPi5nXHUwMDE42XmCXHUwMDE1XHUwMDAzIUn74rU0zlxmMyw2IFx1MDAxMqhYj1x1MDAwMCFcdTAwMDNlXHUwMDAwrdJcIlnygcpyW5pUuIa1WrXZXHUwMDFlXG5W0olMXHUwMDA3p4W39+P7Puu3bq0sXHUwMDFh1fUoV6GV+pdqWTxfvVx1MDAwNauL83wk6MBcYumENlx1MDAwNlx1MDAxND952Vx1MDAwM1apXHUwMDAzjUaQY8xqXHUwMDA2K01Fcvi9lNegYVx1MDAxMKksMEpcdTAwMDBoR+xxgoonZ16hKiHQRMqxUlx1MDAxNYBcYjFcdTAwMTftO1RcdTAwMWRJY1x1MDAxNDtoPydUjUgsXHUwMDE2+EJcdTAwMDJpJeT4Sb6TxtZOY2vvYHf/5jh7lFx1MDAxN7lneX605GDV4K1vJzRX6ND2eekqQKk1Y4RcdTAwMDVcdTAwMTdcZsXM3dvAeimEnlx1MDAxYljRSkOCZe8nXHUwMDA1KyWnvdjYgEOYgGWSoULrRu+F1fPTs8v80VWrjodPS1x1MDAwZVYrXHUwMDAy0pbh4Yz3XHUwMDA2bS9WkVxyL1tV7SyHcYyT6VJeXHUwMDEyLtmvmlx1MDAxN1ZZliSHxPie9Wqqx0o6UbNqUFx1MDAxMlmnjFx1MDAxZmWd5MzVYSV/U9k938k837ab1y6iXHUwMDA0rL6V7ThztIKgXHUwMDAwpFJAXHUwMDA2vVbqJTFLclx1MDAwMTgvs6y1PMV5jlxcRyNcdTAwMDPHYZZX4GRUTD2+XHUwMDAyVstcdTAwMDDNNzWPXG6MjnHQvytX45xcdTAwMDWcS23l5cTQJOP9cVbVVm+fNr82M/fb59tfT7ZKZ/NMMlx1MDAwZb9hd9iX7354klx1MDAxMZO9bClYN1x1MDAxMlx1MDAxODF+TJj+mJc0x1xiUqZJmKFAWWG0L1xisveC81x1MDAwYlx1MDAwYpFGSlx1MDAxOFx1MDAwZZhcdTAwMDBcdTAwMDaLs07rd8hH8sszXHUwMDE4Y2jIXHUwMDAya1x1MDAwYqN7LkpJP+aGplx1MDAxYVH0XFz4mkmshaVe6E+WaEw3XHUwMDFh/YnG3Fx1MDAxNElFJWWSLJJcdTAwMDEhrFx1MDAxNuOHvMeZ6PK2urlXOz0pn12VVk4zN/nscjtmLFuBJasth6LaOWN7ef3SYoDs7CiUgFx1MDAxY/xOtVxc56VcdTAwMTY5xDEzgWGnT7BMKVx1MDAxZrfi8OSMXHUwMDEywnFIKzTH6U5cZjpmyidtOEpZbqlMxWryXHUwMDEylORo11x1MDAxYZRKqFx0XHUwMDAyiIddsV68P195PG89Xrebh1FUeWjN2Cmby9oy1tVWO4UgjZZcdTAwMWPi92BVkVx1MDAwZZBDXGb5Ld6FfiFasqVlUmhfTsS3xLy/1qB0zv1cdTAwMDRryyhZsI2w1lx1MDAxOFx1MDAwYuPzYS42L53UT7BfrFx1MDAxZcv1c7neLlx1MDAxZp4svVxcXHUwMDEz+Fx1MDAxNaPgWIU5xyFMb9LVSzU/dDZR5Dx+xfyoalx1MDAxOEjl2CfloE5rXHUwMDAwXHUwMDE4YoX8UiQgskpY9lx1MDAxNy1cckg1O1fOL2Waw+qUX0LtP5MuLEt4pZ2T/S9zQplcdTAwMWVBdVOJnqVcdTAwMTRoXHUwMDA1mHh9YZRor+7fXVx1MDAxY8r26flDTWy3rlx1MDAwZlx0sVx1MDAwMctcdTAwMWXlZVhjXHUwMDA2noPKVpmkYC+lN84z2lx1MDAwNb6K7UChXHUwMDE22s3Cu5yG6SZYs/ty5IKZboXWYeZi5XRtu1x1MDAwNNW7jWN+zc/H+9OTvH5ccjuPYUcloYbfsDvsd82yqIhcIpnpZm3/4S6bXHUwMDA2lNPG2lx02lWkPufl1U8yVT9ZXHUwMDFi0Kz00yyIbpI9VH415i39KH505uk90eRHWNu50+S1SF7B4pRcdTAwMTakXHUwMDEwx09LRe58y17WWpXN58+5q0+5i82tKImJsUyyyY4/kSSQVkiUvYkpPlx1MDAxMXSWt4DlmExN10dmetfBXGKNPnhcXLDnXHUwMDEwfn740l5dfb4o7aysRcdcdTAwMWKHub3bcHqj+WvYdzTsKIdk+Fxyl9AhUTpR6ZGViJMszE9/ysur8kSaynNcInCzUnkz8Ua01Vx1MDAwNMLJJc+4z9hcdTAwMWJZOO9+hP2eO+9cdTAwMWUokXfv0Fx1MDAxOHJajC+ZT1dP5d3jXG6gK2Tz+0dcctkw2fz7oN3rXHUwMDAwkP1cdTAwMDF2v1x1MDAxZItnf57SYEBWKOUkkmDJmF+eUmtcdTAwMTlcYoNgXHUwMDEwWVx1MDAxM0DM6YhcdTAwMTHvNV9iXHUwMDFjkTZcdTAwMWPokVx1MDAxOFJ+UKCs027BoYRcdTAwMDGn1Fx1MDAwNML7dty6WGewfthKUlx1MDAxYeI07pFd29a/NL9cdTAwMWWph3ZOZa5lTX6+XFy/uFh25r3HXHUwMDAwaWLAcrTvpO1cdTAwMDMsXHUwMDA0YKywXGZcdTAwMDGNen7VMq1cXKDQovJcdTAwMGLGKL6ONF7cdc5cdTAwMDFbXHUwMDEyy3hGUv1oRY7OeziSXHUwMDBiwapcdTAwMDJQM1slkkJcdTAwMGa1NpFcdTAwMWVcboJDPclcbmV8XHL7aaWZrZePi5vnj7uVtdrGp690t+QsXHUwMDA0NjGBdVx1MDAxNlx1MDAwMY10IJXp5TIri1x1MDAwMVx1MDAwZqJQa4NcdTAwMWNo4bRcXOZE4r0mzzpSgr84I4d0XHUwMDE2lFx1MDAxMFx1MDAxOKklq1/tXHUwMDFjuXjrk1dcdTAwMWFcdTAwMDKAdKhcdTAwMTesV1x1MDAxN4VVh8mpXHRcdTAwMTZRYmtcdTAwMDPjpybOro4q2Vx1MDAxM1co1Vx1MDAwYitcdTAwMDeZ3U27+1x1MDAxNZeed69cdTAwMDKPUCNAsONDivqwKlx1MDAwM9ZyymotSKrp8lx1MDAxMims+1x1MDAxOSCVpc5cdTAwMTH/XHUwMDE1uGD/fWFQdYlcdTAwMTU4VEJIXHUwMDAxMD7N8su5OVxcKa1cdTAwMWPU90pfi632Sq7cLN4tOVItXHUwMDA0WmhALaxfx1xyXHUwMDAzQFx1MDAwNb8kz1x1MDAxYlxcS1O2Yk4j3c9cdTAwMDCqRiqrLL2BKrM0QFx1MDAxZOGrJrZcdTAwMWSQmpBgolx1MDAxOOvgYCVcdTAwMTNlbr/uPt6b/U+nlXpNbL+xXHLpXCJJ9zbo8Pek0X49f1/HXCJCXHUwMDFikFGsblx1MDAxOcxg5Pz8VTRcdTAwMTSwl+GZhOBLwkNcdTAwMDCrPVx1MDAxMY1naDtsIVx1MDAxMFx1MDAwM/6qNJZxTKRcdTAwMTbMub+9P6jIkthcdTAwMTD7N7vV0+yXyu7Jp+5b6sHjJEnLmVx1MDAwZjsqaTn8ht1hX75cdTAwMWIlvHNPWpJJ5mVcdTAwMDJcdTAwMTkt0NnxbUz6Y17SrCWbj1S5VTogXCIgwYrMoJnjYlx1MDAxOVx1MDAxYSm38VYlL4LK8bFmV1xyXHUwMDE3767Hqkhv5PLLQFx1MDAxYcdxXHUwMDExsNUkNp+q56o0Mn9cdTAwMTh/zHNm84+wRlx1MDAwM2z+cIpcXKWRid1cdTAwMTaMXHUwMDA1/uBcdTAwMDTiWL86XHUwMDE3XHUwMDBlz8zuZeuomTvcKOWz0cZyu3ygfdHU8lx1MDAxN5IgXHUwMDA0qD5Z9JlcdTAwMThAJdnP8vHJVFx1MDAxZV9cbptcdTAwMWZcYtHPQrGDXHUwMDE505DxfI/hXHUwMDBiNMuqRf5ucNFcdTAwMWFcYt/01yyc6eCthptALlOhmsjmd4lcdTAwMGLAQLKDXHUwMDAziGp8mJrjzY2mOP1aKT2stHdcdTAwMTGuXHUwMDFmSup+6Vm/nZ1iiP/zvTbQ8S/0dlx1MDAwNnHCXHUwMDA1gFKC8ett3Vx1MDAxYzeUmFxym99cdTAwMGbgJP5cIv4uXHUwMDA38fdcdTAwMDex+V1ipYyjXG5jhJ2gJ8Xh5Vx1MDAxNT5cItjbp88n7UyYscfl+vrSyzVh4Fh/WVx1MDAwZehZtEH0eoNeqn3mRYBcdTAwMDZcdTAwMTNfXHUwMDA3O3suv7K+14+z7Fx1MDAxNzlcdTAwMTVPxCWS+c1ghVtpYJy8qS/FL6nunJsxnT/prXbOXHUwMDBlvM9cdMU6vVxmTsmdvKTfzcw5sFx1MDAxM3ScuXiWpcszudW42C192s/WcpWd99G7Vvt9kzjqY1x1MDAwZlxydN/CbemMX4PnwDdQ0qzyZuBhTkHLXHUwMDAz4djnXHUwMDA1WjAtb1/kLteal+p0o3L7eFx1MDAxODZcdTAwMGJPutTtstxcdTAwMDO5SbIwv4b9NeyoxNnwXHUwMDFidof9rlx1MDAwN2dofFJValwi209TYubMt4OUksZffJD+lJdWl7IvlKZLwVx1MDAxN8VnpUtnwvdcdTAwMTNcdTAwMWOzcyxml7xLV/fFv0++31xi12BcdTAwMDF8v+RilFG+i9dcdTAwMDR+XHUwMDBlbd5dXHUwMDE3zvbWqfglX4n29s/3dlx1MDAxM1uWLlx1MDAxN99cdTAwMGZE4FxmXGKjhe9MKmNdsTqRjPeE/Fx1MDAxZbdW+35mMD82rla+RYe0VnC8KmxM+mJ0P1xiXHUwMDFjz5M0XHREgYP1KIlcbv062Fx1MDAwNYuu8f06ZmVL0iuoyV3LJOtQ313WjV9B3dws2Mdy9sneX8rdtdXSefnpxC053Vx1MDAwZnyvZd+e1oKxMr4v6je8YmB8/zZcdTAwMGXBnW9cZjlHvFJcdTAwMDDI01CgXHUwMDFjuWHL6MFcdTAwMDZcdTAwMTZcdTAwMDU4w+FcdTAwMWNcdTAwMDdxclx1MDAxMK+ao1xiJFx0i6amWICZ4TWNmpKywMuT01x1MDAxY6hcdJrsrWx9Pmt8LbSL5nzncdvIjcuzXFz0XHUwMDE2tC6Qm4K+0Vwi61dcdTAwMDPot1x1MDAxMOjrXsp/f1x1MDAwMM5cbuf33o5b/1k32mV/XHUwMDA2pee/alx1MDAxYq9cdTAwMDHGqSmkpG88pFl76njm/rUntFwiYlxys3By6qKgSoksKraBrN7dXHUwMDA0u9qf4Vnt071dWa3uX1x1MDAxZdRcdTAwMWWbd4XjZpKnvixI1a7TXHUwMDE23PBLVtag6stoXHUwMDAyXHUwMDA2orPNXHUwMDE2P1x0RtN0O/KltdmdXHUwMDAxVFx02Vwiszgtmke1IKhKkbJTu2CsXHUwMDAyXHUwMDFim/FcdTAwMGJra7dnj2H2aKNw0FxcvTtcbqN2e+UmKVx1MDAwMb8sYLXsK7K2s8Yho9U521x1MDAwN1ZcYpRU7Eii9WCdikSV2md3XHUwMDA2YDVglVx1MDAxNvGC/ftcdTAwMDNrusuarFn5PVx1MDAwMitcdTAwMWRN41x1MDAwN1rR7v6FuSiXTu7yrvX5oZy5XFw3XHUwMDA3S0/665CHtGVb70j5/d178CqFsYHjqMY3aFx1MDAxMHK69oMjSH8qkMR4XHUwMDA08MyJYYtUtPRcdTAwMGK8wCm/XHUwMDE3sVSDi1RYu0jPXHUwMDFmgDm0hn45MTTJuHef39v7slx1MDAwZke3mYeHwu1D+2v1tjF97vK9XGY7KiU6/IbdYV++XHUwMDFipVx1MDAxM3BWOiFxa3KV2JBcdTAwMDVcdTAwMTjbXHUwMDFjSdP4XHUwMDBiK9Kf8pKmREHadG1gtd9Lxlx1MDAwN4ZcdTAwMWHQzrNcdTAwMTVcdTAwMWONVFx1MDAwN8O4hFx1MDAxY15r86adXHUwMDEyp7NYXHUwMDE4S5a/uS+wY1fW51x1MDAwZXhAXHUwMDE3V7UjuYSPXHUwMDBiJFx1MDAxM46wcoNkwsdp2IRp7F70q7xgXHUwMDAyPkflrPJ8eYqVK5cp5FpHn3dL9XJ5ud1JXHUwMDA2XHUwMDAzXHUwMDA3N563Yp3xLYJVv0D6XHUwMDEyhjWecKuJY/qp/MlcdTAwMTRCIYuWIa2VUsbK2HL0eEZJd1x1MDAxYTmBYdUh41x1MDAxYkl9zyhJQyB/SOwzs/XOibyj5Iy95qjUM9HHhmmzoj+3r0ru6GrroJJTh3hw/by29LSjXGaA8cuZlFx1MDAxM35xiO03XHUwMDFk6L03v+LL75dn58c7mlx0mZA63YTe5EdOyzqahPtcdTAwMWGbx0/NOvoxXFxCflx1MDAxZonuIN/MeUU3vlRHVysnbnPzZrN+9OX86Gz1dOP8aHXppdrIQHg2l187KGz/JvZcdTAwMWShdlxuXHRcdTAwMWRcdTAwMTA//DluxFwi/G5cdTAwMTVcdTAwMWOFalwi8LtFqGE2XGI5WGXH1SpU7Fx1MDAxYmhcdTAwMWOQa09qd7pni/Nfgv1cdTAwMDNcdTAwMDU7k/Ja/WfghU4o2iPK7GmbLfL9/Fx1MDAxMo2x5Xun0ag8rjdk5n5/7aZyer5xXHUwMDFh3W++jzK7XHRYvK1Fllx1MDAwZYfxXHUwMDFkKF9k3O/J6Jv+k1x1MDAxMobN99xk3PcpXHUwMDAxLYxfy2ZcdTAwMDTqYYxhwsDybKzwsZ52XHUwMDAzIaBf1sUgolx1MDAwNW9cdTAwMGVmJbpJXCLAt6PWqcSQXGL4Vfq93cavsl9X7E21JU736qtHxbtnVXu8v3xTgn2xcFx1MDAwNe3/Ut3pKY5cdTAwMDMmSTFcXK3SSrJTXHUwMDAwen6kLU9cdTAwMGLhgFx1MDAwNpTPXHUwMDEzW6GG0EJ8W1x1MDAxZOFcdTAwMTkqwneCwngg8lx1MDAxYVx1MDAxNfl9j1x1MDAwNIpcdTAwMDWnLCa1SKmATeus41x1MDAxMuvsfomCJ++MX1x1MDAwZrpoP118udtsVWBlz62V98rm4K603Fx1MDAwMTzrhYBcdTAwMTHg9SqrWCn7o1wi0lx1MDAwMVjR2fC4t0nYjNvqsL8qXHUwMDFjKE/NxyHrXHUwMDAxfTXIoNDaoensXHQ2WFxyQmfYu47npX4qoDqdXFxcdTAwMGLyPVx1MDAxNk18O4GRXHUwMDFkoGzt881u5ejE1Y+b+TD6VMmJJc808fNcdTAwMGV8Po3YzKPyPn0/Ulx1MDAxNYdcdTAwMDLWOWPQTFu4TGurMz1SwTdcdTAwMDNmQ/iTXCJVxnfY6ocqR8BW9qzaXHUwMDFkzbTbyVx1MDAxZEa3ollcXL9ccq9BrV/Jh+aSY9WpQGnyvo6RxJI7oFUx4JDUNyxjZ9HAdGo1rbHO1GBcdTAwMDVgpLr4XHUwMDFlQ+9cdTAwMGarI2ihKVxcZn5cIuArumOD9TjKStrP5chd01x1MDAxOYX526eVqkhcdTAwMDDrXHUwMDEy1dhd4DpcdTAwMTU1wSGnjL3rb5vag1xiUDFI+Jx05KZSrSNq7DqQ7GyA51OzN+KGRFhaXHUwMDA27KZ4rq5veyjt4EJr3+uCT8/DY305MbRsfV3BXHUwMDFjfcpcdTAwMWTa7fVjzK5uhZtccnszfTX8v3zYUbX74TfsXHUwMDBl+/LdwnRNYlx1MDAxZiCZzOf1IZiRQo+vZtJcdTAwMWbzslx1MDAxNu9BpatcdTAwMTlcdTAwMGWcXHUwMDE1XHUwMDA3rEJcdTAwMTjfXHUwMDFicY5qhkarmSHFe3ROk1Y/YvvQN+Vme1x1MDAxYlx1MDAwMVx0tvNGKfZcdTAwMDaI/1xi6rkqvXhcdTAwMWbFn/Scq/cj7OeQ6n00RfneQvJcdTAwMGVHXHUwMDE2pfKZj/EzrNnzfFttnn2l/f1wd0OYnZvPoV1uR1x1MDAxNYVcbsjvU+1cdTAwMTe/KG1s31xuQ41s+i1pY3zLrFx1MDAxZVx1MDAwZeaM+1x1MDAwMVx0XHUwMDAyo3zlnuLNe+OJKl+0l4o/woe6XHUwMDAzjqpB1Fx1MDAxNlx1MDAxMN5FUPXbi0X6mG82j1wiXHUwMDFlkk9/gy7Pulp8kdruMFx1MDAxZu+r4cPqsEC18/FcItBcdTAwMTFcdTAwMDBcdTAwMGay0M/5r79/+/s/t9XlXHUwMDAwIn0= events.Key(key=\"e\")events.Key(key=\"x\")events.Key(key=\"t\")Tevents.Key(key=\"x\")events.Key(key=\"t\")Teevents.Key(key=\"t\")TexText"},{"location":"guide/events/#default-behaviors","title":"Default behaviors","text":"

    You may be familiar with Python's super function to call a function defined in a base class. You will not have to use this in event handlers as Textual will automatically call handler methods defined in a widget's base class(es).

    For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Up and Down keys), then Static.on_key, and finally Widget.on_key.

    "},{"location":"guide/events/#preventing-default-behaviors","title":"Preventing default behaviors","text":"

    If you don't want this behavior you can call prevent_default() on the event object. This tells Textual not to call any more handlers on base classes.

    Warning

    You won't need prevent_default very often. Be sure to know what your base classes do before calling it, or you risk disabling some core features builtin to Textual.

    "},{"location":"guide/events/#bubbling","title":"Bubbling","text":"

    Messages have a bubble attribute. If this is set to True then events will be sent to a widget's parent after processing. Input events typically bubble so that a widget will have the opportunity to respond to input events if they aren't handled by their children.

    The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the \"No\" button focused, it will receive the key event first.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbVPa2lx1MDAxNv7ur3C4X3pnarrfX85M54yioFSxVk+tPZ5xYlx1MDAxMiElJDRcdCDt9L/fXHUwMDE1UFx1MDAxMt5cdTAwMDIqcHBu80Eh2eysvfZ6nv2slZ2fW9vbhbjXclxuf2xcdTAwMTece8v0XFw7NLuFt8n5jlx1MDAxM0Zu4MMl0v9cdTAwMWVcdTAwMDXt0Oq3rMdxK/rj3bumXHUwMDE5Npy45ZmWY3TcqG16Udy23cCwguY7N3aa0Z/J36rZdN63gqZcdTAwMWSHRnqTXHUwMDFkx3bjIFx1MDAxY9zL8Zym48dcdTAwMTH0/jd8397+2f+bsc52zWbg2/3m/Vx1MDAwYql5nODxs9XA75uKOUKcMCT1sIVcdTAwMWLtw91ix4bLd2Cxk15JTlx1MDAxNXrRt6vuXHUwMDE34pu8yI/l/snt58reSXrbO9fzzuOeN3CEadXboZNejeIwaDiXrlx1MDAxZNeTu4+dXHUwMDFm/i5cbsBcdTAwMDfpr8KgXav7Tlx1MDAxNI38JmiZllx1MDAxYveSc1xiXHLPmn6t30d65j6ZIcVcciQ0JZprLDVVw6vJ77UhMeJcdTAwMTQrjSTmmo3bVVxmPJhcYrDrP6h/pJbdmlajXHUwMDA25vl22kZcdTAwMTFL48yYu4+jVdRgjFx1MDAxMlwiwVxmqlx1MDAxMeHDJnXHrdXjpFxyIYZCTCjJXHUwMDA3t8qY4vSnhGtFqVx1MDAxNul8JbdvXHUwMDFk2f3Q+GfcoXUzbD04rlx1MDAxMCVfMqYnVlx1MDAxZozHVTa2MpNOxFWl7Fx1MDAxZlQ+fyzvX1rfj3ul8/tw2NdIIMbOfVxcXHUwMDE4Xvj19ne3M7tcdTAwMWRp/XbRXHUwMDFiLmituL+6b1x1MDAxZvPDkPpnJdwrnXlfet3p1pphXHUwMDE4dDP9PnxKo6ndss1cdTAwMDEjYCEoSyiDS8SG1z3Xb8BFv+156bnAaqQkspUxeIK7RsafIS5G0fjZR+JcIohSXHUwMDA0WOApiOZcdTAwMTFX/vRtKnFplENcXFx1MDAwMlx1MDAxOVx1MDAwMlx1MDAxMyBcdTAwMGLBXHUwMDA2zPVcdTAwMTLiikPTj1pmXGJ8MIW85HzyXCJcdTAwMTNkRVx1MDAxMJFMJ3S2fLrKj05O5Vx1MDAxM6IzXHKCwI/P3Vx1MDAxZv2lUVx1MDAxOFx1MDAxYyuGiIBBaI24XHUwMDFjaVUym67XXHUwMDFimdd+XHUwMDE4g+W7rdab/2ZdXHUwMDFkOWDCYLlcdTAwMWRpvOu5tSTOXHUwMDBiXHUwMDE2XGbKXHRHIFx1MDAxMLsgXHUwMDA0hlxymq5te5lwtMBcdTAwMDJcdTAwMTP6XGaPXHUwMDE2WZOD0K25vuldjFx1MDAxOJhcdTAwMGLJXHUwMDAxJUzBpFwiszGJwfGYscyiNVx1MDAwZpP5JLWhmKRSXHUwMDFhhDIsZKJcdTAwMTVENjL6XHUwMDFkMGJwwCsgUilBJScrQyU1XHUwMDE0cCSTQlxugShXelxuKqk2XHUwMDE0xVopLjSGcJ5cdTAwMDAp5lxcwigofjpG+6Y+XHUwMDFio09bQTJ2mGG85/q269fgYrr0ParkRTDRR7HVTqzcXHUwMDAxhlx1MDAwNYBzjlxiU8BcdTAwMWNEZFx1MDAxYdXMVlx1MDAxMvRcdTAwMDZcdTAwMTeKSFx1MDAwNbHNlCZYPDRcdTAwMTiuwFx1MDAwNce355tUvPlcdTAwMDbS8NI8s8t2T/rt3a87++VZJjGsMdZIYyqJXHUwMDEyRLFcdKswg+mHmcNEXHUwMDEzglx1MDAwNPydMMszo7hcdTAwMTg0m25cZs7/XHUwMDE4uH487uS+N3dcdTAwMTO011x1MDAxZNNcdTAwMWW/XG7Dyl5cdTAwMWKnhVbS46h4TD9tp7Dpf1x1MDAxOX7+5+3U1juzozk5JuI47W8r+39cdTAwMTajhY5cdTAwMTVcdTAwMGbwPIXVXGK4eFx1MDAxNq1cdTAwMDGSXHRcdTAwMDaxsbjSyJ/nXHJlNaKUQSVcdTAwMTJIJ1x1MDAwYp5iZIzVNJBcdTAwMWWiSFx1MDAwM2q1TtbF1WlccpHOxZDGVKp8XHUwMDFleFx1MDAwYphVipHVZi2pkGp8iM5kpV5FOLBkLL+dWl9PXp5cXHjNXHUwMDFmn2/+wlx1MDAxZr2DYlxye3Xkur1ytJhcXM/t97LyudM9ZvsnxyG3yz3SJmxfLKFfcmlcdTAwMWZcdTAwMWSWXHUwMDFh1onaZfii6Z1cdTAwMWX4X2tL6HdF7n1d3TbE5VGn1PqEb9pRvdHhJXRXtf/vnPuCXGZ2vebOy+On33BBa0vlxoUon3+7u6yfdSp+3Tv+XHUwMDE27CzBXHUwMDBi91fki/zavjmpoHL5XHUwMDE2k2bN6Z0tqT7AJFx1MDAxMZCOrro+QIhW46dcdTAwMWZXbYq51ELRxXOR/LDY1FVb07xVXHUwMDFiUjJDrmnV5lNW7Uxq9LBqKyk4XHUwMDE4K+g6K1x1MDAwMkxohTR6QjxOr1xiLFpcdTAwMDEoPqbnb6795IJrv79OKvReULsuXFz704tcdTAwMDOZPHGkOOA5d6PR/6TSwFx1MDAxYy06Xlx1MDAxYZhr+fM1NkWKzkIrRlxcI0GzYTFcdTAwMGaunVx1MDAxYoJlLf7c2bPo0W3ZP2qflXv/Llxc+Ty0YkFcZqIxpDmSUsGoXHUwMDFhRSucMrhmSlx1MDAwMVx1MDAxMFx1MDAwNWFMrlx1MDAwZaxcdTAwMTkwpFx1MDAxMluMg5VgjDBkY5lq31o09sVheFOMnfaN3K1Wrq7qny5+XHUwMDFjfvitsZelsVfk3tfV7ao09uvywqo09uvygqf3iuq2XFxkRSGc84tcdTAwMGb3tVJpg72w/JRgXlx1MDAwNjN9IGm3XHUwMDBmn3JcdTAwMTSYwpqwpyiwXFyhMSsjoCSjcMc1XHUwMDA2wVRRgejiXHUwMDFhI3/+NlVjyHyNodelMdRcdTAwMTSNISc0htKYScroXG52NMxcdTAwMGJH/IRwfFlCsNeO48B/k5xcdTAwMWLo6uvClVx1MDAxM11cdTAwMTfeXHUwMDBlvnXM0DX9XHUwMDE4pHbUtixcdTAwMTjd7CxBjna+pCxhjphcdTAwMWXPXHUwMDEynjecXFxE56dcdTAwMGV49lNHjFx1MDAwNYQ8hPvimf7ljYjrxdZt1W+ffqrY9ztcdTAwMDeXZ/VNz/SpVlx1MDAwNoaoXHUwMDE1QkHSz4SczFx1MDAxZJCAbjjTjJOV7lx1MDAwNVgsecCISkQ5ZWtOXHUwMDFl0MfK4Un7qnL48axcdTAwMTL39lx1MDAwZXo8OPF/J1x1MDAwZstKXHUwMDFlVuTe19XtqpKH1+WFVSVcdTAwMGavy1x1MDAwYqtKXHUwMDFlXpdcdTAwMTde8DjhmTnJ9IGk3T58yntKwUEkp09cdTAwMTBWlpPgmTlcdFGCXHUwMDEzQcjie1x1MDAwYvKnb0O1XHUwMDBiQzRfu+i1aZdcdTAwMDWTXHUwMDEypSmiXHUwMDAyr0C6LDNcdTAwMWWXnpRUgylcIt5cdTAwMDG8huvOSOZo9Fx1MDAwNTKSeWPJXHUwMDA188z9j1jJmc9cdTAwMWMx0kJLxjPhPVx1MDAwZs75tLmhcKaKXHUwMDE5QiCtklx1MDAwMoLi2YpK/6Gjklx1MDAwNlx1MDAxNZJizYHeOFErrDFgXGaWXGJGXHUwMDE4p5RQwtkkulx1MDAwNTe45lopRGjyglx1MDAwN1x1MDAxZFx1MDAwNztnyXNi8ZyNRM/fXHUwMDAw+Vx1MDAwMrAvuFx1MDAwMXLh3YbIYJgqrFx1MDAwNeVIJvOVaTPYaUhcZlxmPoZlilxuLlx1MDAxNOTakztcclx1MDAxN9pcdTAwMDCZXHUwMDBm6lx1MDAxMZO4XHUwMDEwyVx1MDAwZUBcdTAwMDSrXHUwMDA1pJFcbk/YXHUwMDA0M4+0XHUwMDA0vIE9SMBcZuNcdJte0+7H2ZGcXHUwMDFjXHUwMDEzMZx2t5X9/1xmOpstTmBcdTAwMDKoktn8fVx1MDAxZZvlXHUwMDE3pjeVzSQxINDAXHUwMDEzXGYpxHXqj1x1MDAwMZkpQzCMOaeKXHUwMDBi/qJXw+ZQXHUwMDE5NVx1MDAxOMijxFx1MDAwNM4kKKUpVEZcZlx1MDAwMcJcdTAwMDR0iVJcdTAwMTIxltnv/Vh0oUpcYs1AvKyXzJgmXHUwMDE5sfQvktlcdTAwMGVQXHUwMDA3lVx1MDAwMCTMQEpKJsgkdYCjKbhZUVxuLCM0aM7n0Vl+1XTMqMQmXHUwMDA07UEkSEz0hFEw/UQgcCeiXHUwMDE45CdWr5rOdmaHc3JMXHUwMDA28lx1MDAxM1x0LbdcXCwzr7OOcVx1MDAxYeeYwaLCXHUwMDE3L1x1MDAxNvNcdTAwMWatfetL7XDvwlx1MDAxM7FC3S46vmttOqdcdEZcckWJgHDjXHUwMDEyKzZZK9ZUKFhQYGXNvlT2nPddmSWcO87YJKVcdTAwMTGR9pxcdTAwMTaKM1vQXHUwMDFlOEtCqqVhTp7xXGJoXHUwMDFlZ1xyw2pKyaLt69L9j6/V71cq+u53j3ixcVJ9eSWkKE87ptM+/G7+1Y0vT2k1qsYzXHUwMDFl+i6pXHUwMDEyMn0gabePiJrN36BcdTAwMDKUeMrjsFxccM6qhEiSkzrBIVx1MDAwNVx1MDAxNotcdTAwMDMzf/o2XHUwMDE2mCpcdTAwMGaYmsHStCRg5qpccsKnQJNMpEZcdTAwMThcdTAwMDNXcs7Uup/OPjFcdTAwMWPTWU9cdTAwMGIhmUeGI4WQdJCPhVx1MDAxMKeT2GR8cHpvXHUwMDFhTu/9deHiujDjXHUwMDA1Tj3y46W9wDlnkVx1MDAxOa92TDd4gMmtXHUwMDA3oFx1MDAxN8xW6zxcdTAwMDa/XHUwMDBlXHUwMDA1XGZMnWs/OCf1ZaHjOt29Kbx+1z+SXvs4T1x1MDAwMOUkXHUwMDEz9/PX1q//XHUwMDAxXHUwMDA3vCMgIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")

    After Textual calls Button.on_key the event bubbles to the button's parent and will call Container.on_key (if it exists).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXG1T4spcdTAwMTL+7q+wOF/2Vq3ZeX85VVu3XHUwMDE01PXdVVddr6e2YohcdTAwMTBcdFx0Jlx1MDAwMcSt/e+ngyxJeFx0iODi3U1ZXHUwMDAymaHT09P9zNOdXHTfV1ZXXHUwMDBiUadhXHUwMDE3/l4t2Fx1MDAwZpbpOuXAbFx1MDAxN97H51t2XHUwMDEwOr5cdTAwMDdNpPs59JuB1e1ZjaJG+PeHXHUwMDBmdTOo2VHDNS3baDlh03TDqFl2fMPy61x1MDAxZpzIrof/jf9cdTAwMWaadftjw6+Xo8BILrJml53IXHUwMDBmnq5lu3bd9qJcdTAwMTCk/1x1MDAwZj6vrn7v/k9pV3bMuu+Vu927XHKJekKiwbOHvtdVVWpMJNOc9js4YVx0Llx1MDAxNtllaL1cdTAwMDWF7aQlPlVwS+dccr0vNre3qvdRdHrepHf1teSqt47rnkZcdTAwMWT3yVx1MDAwZaZVbVx1MDAwNnbSXHUwMDFhRoFfsy+cclSFdjxwvv+90Fx1MDAwN1x1MDAxMyTfXG78ZqXq2WGY+Y7fMC0n6sTnUDI+06t0ZSRnXHUwMDFl4lx1MDAxZVx1MDAxOFx1MDAxYlgozjiTQlLBkvF2es1cXCiKNSWSQC86oFnRd2EmQLO/UPdIdLsxrVpcdTAwMDVcdTAwMTT0yklcdTAwMWZFLI1To27/XHUwMDFjr6JcdTAwMDZjlFx1MDAxMEmJplx1MDAxYVx1MDAxMd7vUrWdSjWK+1x1MDAxMGIoxISS/OlSKSPZ3UlRQlDCiEh0jK/f2Cl3neOfQZtWzaDRs10hjD+kdI/V3lx1MDAxY/SstHel5t1sX3ok2tza31x1MDAxMZfhiSutVl3gvqyMK0b2Q1ToN/x4nyf24XBcdTAwMTe7Zpl9uSi192osujQv9uhosWZcdTAwMTD47WnlLkjd31xcbKb3+2kvmIjtvUuctNkom09Yg8GtXHUwMDE54kRwOPrtruPVoNFrum5yzrdqXHQ8raT0XHUwMDFkXHUwMDAyxYyeKUTkaixcIjKlkYbwTHSYhIj5Vl5aRFx1MDAxNLmIKIjBJII5QfLliFx1MDAxOFx1MDAwNaZcdTAwMTc2zFx1MDAwMHBmXHUwMDA0KsrJqEiGUJAqzVxilkLPXHUwMDFmXHUwMDA15+mdiVx1MDAxN/hedOo82l1ZXHUwMDA2x4ohXCJcdTAwMTCRWiMuM722zLrjdjJcdTAwMTPbdWPQfL3RePeftKVDXHUwMDFiVOjK5JnO665Tif28YMGg7CBcdTAwMTNcdTAwMDKRXHUwMDAzXGaj36HulMtuylx1MDAxZi3QwFx1MDAwNJnBzjSrvVx1MDAxZjhcdTAwMTXHM92zjIK5IfmE4iNiUlx1MDAwYjYuJqmG6eaYTc9S8peVJY1JgpBBXHUwMDE0xKRElFx0hjHLxCSh2kCEMM41XHUwMDA355dcXCwsJpGhpeSKgzaMyVx1MDAxMVx1MDAwMcm4QVx1MDAxNVx1MDAxMlxuaU0wk1SKwVx1MDAwMMVUUIlcdTAwMTVHM/CUrqazRiggXGKZJULDyFxmolxyxys7Xlx1MDAwNVx1MDAxYZN17yf5niZcIroxbDXDrlxyXHUwMDExo1xu0JMpXHUwMDAwUoKFZDzVrWI24oXIoFx1MDAxOMGUY1x1MDAwNeingIj3OvRcdTAwMTfggu2VJyvFOo9cdTAwMWN92l5v75OrWqd0XCK+XFw0ySil1kArXHJTg5HATFxizaRcdTAwMWVWXG5TQ1x1MDAxMVx1MDAwNViHOcFYa4yHtHLNMCr69bpcdTAwMTOB9Y99x4tcdTAwMDat3DXnelx1MDAxY+xV2yxcdTAwMGa2wqjSbYOo0IglZilp8m41XHSb7of++3/ej+492pnjY9iNXHUwMDEzYSvp13FoXHUwMDE22Fb0XHUwMDE0zCNcdTAwMTCNUDpcdTAwMTbSMIQvl1qpXHUwMDA0KiZhWv4kLymmQXppYIUhX0GUUKF1lmdcdTAwMTCtXGZCMSyGXHUwMDE4QStTfECzOfJcZpFgVFx1MDAxZseUXHUwMDFhwi2CtITVJoV6r5Jfbd7W6N3mbadxb53csk7rlG6ffVre/Opi97zV3melg/2Al7c7pElYScxBLrko73zaqllcdTAwMDdqneGzunu06V1V5iB3QeZ9W2Jr4mKntdU4wd+aYbXW4lvo9rD8x7hLLfaRblx1MDAxZu/fsiNyXiu5XHUwMDFi9p44q1x1MDAxZOzOo0Bycllj1YOz/a/0+HHnq/ulxYLSi+ROKlx1MDAwZYw2UFwi9ueCm0PuuEB44cVcdTAwMDFC2fhlWyhcdTAwMTmTXGKS5J2Tlu18v1jWZZuSvGWbXHUwMDAyRZSvtGzzXHUwMDExy3YqZe4v20RLyqRIxvtcblx1MDAwNVx1MDAwMaZcdTAwMTTCXHUwMDFhP8MjR1x1MDAxN1x1MDAwNKYtXHUwMDAwXHUwMDE0f2bn7669uMEpf7yOK/+uX7kuXFx7o2tcdTAwMDOcZOT0U3/Xvs36/7MqXHUwMDAzXHUwMDEz2OhgZWCi5rPTbHDG8fc3XGLTXGLy0OnD9ZN9ZNPHc+fh6/bxSXX7xrNcdTAwMWXto19cdTAwMWKufGK0XHUwMDFhXHUwMDE4XHUwMDExXHUwMDAx2CgohFx1MDAwMYCTyEQrXHUwMDEz3Fx1MDAxMMCyIVx1MDAxOedap1x1MDAwYlhzXHUwMDBmVo2Gg1VcctdcdTAwMDY0xZhS9dr3ME7XnMvS/tq3b0FJ31xir3VPXHUwMDAzbP3h2PPi2Fx1MDAwYjLv21x1MDAxMrsojv22rOBeXHUwMDE2vfXO1n2FXHUwMDE3afXIosXjqIh+PyvojaK62S6yolx1MDAxMPbp2d5DZWurvbxWWFSqMXd1J2VcdTAwMWGjL5iI7b2bJ6/LpS/jMlxySphcdTAwMWE8nTBcdTAwMTeluKY0YbqTmEu+mZeTuYgsc8lmXHUwMDE5TKLX4i1qXHUwMDA0b1x1MDAxOXFPQ1NcdTAwMWRXilOT8n+YZGw0o8j33sXnnrj6deGrXHUwMDFkXlx1MDAxN94/fWqZgWN6XHUwMDEx0PewaVkwuvGZh8xcbp9T5jGBoVx1MDAwZmZcdTAwMWWzXHInN54npCNCjlx1MDAwYmrOXHUwMDE44/g5NzLXLiufxVWntLPjXHUwMDFjuvbnzm7n7mHpq1x1MDAwN4RcbkNThFx1MDAxNCVcXGlAuYG4hnxcdTAwMDTBeYw4U9BXysVcdTAwMDX2dFx0XHSDMVx0qVx1MDAxN1A7yKWK+uiIq8+HzZ3L5teaf2Fe3W3JP+nIvNKRXHUwMDA1mfeNiV1QOvK2rLCodOSNWWFB6cjbssL8b3wsSN1JWc7oXHUwMDBiJmJ775Ygy+Fjs1x1MDAxY6KB4Ovn3E7JN/OyXHUwMDEyXCKGc1x0XHUwMDExJDqvRYimzHRcdTAwMDRcdTAwMTJUKM5cdTAwMTdQoV3qTOfQXHUwMDFmkVx1MDAxOdiAXHUwMDAzwWunOVx1MDAxM5j/XHUwMDE0ac6kseRG89h9mlx1MDAwNOHxt0fBwbHkWj5j93QuXHUwMDFjL2s8XHUwMDEzajBcdLFEhGSMZm+3UKVcZsA0QZiIN/nSXHUwMDA1XHUwMDA2M8aGXHUwMDEwglx1MDAxMcYpJYAtbDi2IdeK94tCZFx1MDAwMdBcIonpUKgrgCNGOZmhqDH7Rs1cdTAwMTeE+pRcdTAwMWI1p95cdTAwMTOJXGaGYfhgXHUwMDAyKjhHhKhUp6dcdTAwMWSRxMBgZULiXHUwMDFlQilGSa/HM/dp5sd0RicuXHUwMDA0LFx1MDAxNTzeXHRMJUpcdTAwMTC6r1x1MDAxM8w90lJoXHT6IFx1MDAwMXP8tndpjvfl+Fx1MDAxOPLiRNxK+vXZaIY104Onkz2aSkpYtNn0ezTzS+jLWYMlYHkpkFx1MDAwNk+iUjCW2lx1MDAxM/lcdTAwMDRn2oh3XG5cdTAwMGLEqKZcdTAwMDIrNqDYPPFcZmBVXHUwMDExrVx1MDAxMEw2k0IlO1x1MDAxN1x1MDAxMjwjhlx1MDAwMColMFdKXCLGdKoq3Fx1MDAwMzREXHUwMDAwk1x1MDAxNVEzlHNmXHUwMDA3NPjHuUx86Vx1MDAxN1x1MDAwMtpcdTAwMWGgXHUwMDA3zKRgmDGAdSZIqtNcdTAwMTN4gJ0pWFlRXG44I7RmM248z6/FXHUwMDBl6Fx1MDAxNKuEgFx1MDAxZmDALZxQ/tXUvnNcIrrPXHUwMDFjUayUxupNXHUwMDAz2tp4b46PYT9+JqTlXHUwMDE2oWFix6NcdTAwMWFnXHUwMDA0ol1PX4Qu31x1MDAxY1/c3947l6XPx7e6yuT5oW3+WlSjk1BccixvSLA6XHUwMDAx6ytcdTAwMDbeNLwnRlx1MDAwYlx1MDAwZcu6XHUwMDEyWCP2omdp/mKWsG85Y8OQRkSCpklcdTAwMDE6wbVcdTAwMWVmcZhcdTAwMTNccvrMsOl8XHUwMDEyZPXdakTNorTl7IR8XHUwMDE37Z5cdTAwMWNcdTAwMWY+3lhHlebGWvHlJZaiPGqZdvPTvfmlXHUwMDFkXVx1MDAxY9HD8DDam0OJZe7qTiqxjL7glNo6n3evLm4qdrBn1qnliK90s/FtOiv8XHUwMDA0gDz6XGZ/iWstqHQj5djKXHJGgoLLajZ9qpc/fctcbiMyXHUwMDE3RjQ3WFx1MDAxYUZcdTAwMTaX7KWraMmDscPpXHUwMDFjQLtcdTAwMDI6J39B5Wam5+5SlVx1MDAxYoIyZ/uVm2QoPys3divWydizO+9qdufjdeHsujDmyVid+fLcnoydsCZcdTAwMGWWZ0YrPPtcdTAwMDKvUyvWYE1cdTAwMTWmgrDnZC3ty9ZR5+BTZJ5vr31ztrfq7EBtLntcclx1MDAwNlJcdTAwMTFDQqIo4u0jXFxcdTAwMTA+UIVBkDJcbowhhWNAhNGLXHUwMDAy8+VcdTAwMGI8dIOo1LM8r/6SXHUwMDA13lx1MDAxNlx1MDAxYid3ev2uuVtad+W3dus4OKsv71x1MDAwMr8gdecudlx1MDAxMm9cdTAwMTh9wd+HNyg9dkc+iZ+Fxlx1MDAxMLfT3/LJn76lhSeRXHUwMDBiT5RcdTAwMWJoXvA0XHUwMDE33iBjXHUwMDEywyVdwHOvf3hDwlx1MDAxYiastXPgXHJja52pZ6NcdTAwMDb3pXFcdTAwMDSrXHUwMDEzXHUwMDE208dkPkg9KybJq8UkV8JAkGjHv66VpfFcdTAwMWNcdTAwMTkqrktROX6TqS3iavDsgSjhXHUwMDFhXHUwMDA0UTzqXHUwMDA3brgwQC/IXHUwMDFmXHUwMDA0RfG2XzlcXNVkQmtMkX7d2zRwSZz6XHUwMDE1g/lXNfN59Gr6llxiXHUwMDAxnGRIi/hX0SBcdTAwMTVcdTAwMWLxc1x1MDAxYVx1MDAxOEBVXHUwMDAzmGI4YmuyXodnVjXzY3RAJ+B1Or6nT1x1MDAwNJNsSCNhXHUwMDAwwnVnlOk4W6ZDXHUwMDFhvama5rBcdTAwMGZ3T1x1MDAwZrtvXCJpJf36bFwiMb6MyblShKR/X2ZcdTAwMTJmSeteXHUwMDFkXFzV/eKRf3/X1vqiendcdTAwMTUuP2ZJXHUwMDAz+Fx1MDAwM9aKgWdjNlx1MDAwMFxcSFx1MDAxYZpJQYVWWDGxuC3yPFlcdTAwMWJcdTAwMTJcdTAwMTYxjFI0vlx1MDAxZFx0gfeqv8slXHUwMDA00UrNhFJTsIjhfSM3zZub9Fx1MDAxMp+mXHIq03vavSCR31x1MDAxOMdcdTAwMTgyo1x1MDAxOKRcdTAwMDc9TZ5ia6VcdTAwMTe3XHUwMDA1s9E4jcBCfYiDSXDKvWEm8lxuLcdub4zIdm+7Ryy1XHUwMDFir3Fk2PFcdTAwMTR8/7Hy419iJnwqIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

    As before, the event bubbles to its parent (the App class).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT4lhcdTAwMTP+7q+w2C/zVlxy2XO/bNXWW4p3XHUwMDFkXHUwMDFkXHUwMDA1dZzXLStAhFxmgWBcdTAwMTK8zNb+97dcdTAwMGYqXHS3XHUwMDEwlTgwO1QpkFx1MDAxYzqdPt1Pnu7Tyd8rq6uF6KHrXHUwMDE0/lgtOPc123PrgX1X+Gi23zpB6PpcdTAwMWTYRfrfQ79cdTAwMTfU+iObUdRccv/4/fe2XHUwMDFktJyo69k1x7p1w57thVGv7vpWzW//7kZOO/yv+X9ot50/u367XHUwMDFlXHUwMDA1VnyQolN3Iz94PJbjOW2nXHUwMDEzhSD9f/B9dfXv/v+EdnXXbvuden94f0esXHUwMDFl13p066Hf6auKmWBMak3jXHUwMDExbrhcdTAwMDFHi5w67L5cdTAwMDaNnXiP2VRAZVxc/LZ3vr7+5UpUrtTa3XkobuPDXrueV45cdTAwMWW8R0PYtWYvcOK9YVx1MDAxNPgt59ytR01z9JHtg9+FPtgg/lXg91x1MDAxYc2OXHUwMDEzhkO/8bt2zY1cdTAwMWXMNoRcdTAwMDZb7U6jLyPeclx1MDAwZt+KmEhLIaQ5ZZxJitRgd19cdTAwMDDllmJKI0a1YFxu61x1MDAxMcVKvlx1MDAwNzNcdTAwMDGK/Yb6r1i1ql1rNUC/Tj1cdTAwMWWjSE3jxEnfPZ+uolx1MDAxNmOUXHUwMDEwSVx0WFx1MDAxY1x1MDAxMT5cdTAwMTjSdNxGMzJjXGJcdTAwMDE9mVCSP1x1MDAxZSphI6c/J1hJolx05yreY1x1MDAxNOju1vve8deoTZt20H2yXSE0X1x1MDAxMspcdTAwMWK9N0ddK+leiXnvKSEu9q/0fveb+vL15vtR8M0hXHUwMDAzWUO+XHUwMDE4OfdRYbDjn4+/xP4kYodGf8x6wKzaXHUwMDFlRrf35Vx1MDAwYreoP92cVnfOTlx1MDAxZZwvXHUwMDA3k7W1g8C/S8h9+lx1MDAxNPt+r1u3XHUwMDFmIVxmXHUwMDBiQVx1MDAxOeJEUarkYL/ndlqws9PzvHibX2vFqLeSUHhcZmyHzj+JtEhMRVrEXHUwMDE4w0igOOhnIW369C0u0pI0pFXCklx1MDAxY1x1MDAxM1x1MDAwMFn2ZqSNXHUwMDAyu1x1MDAxM3btXHUwMDAw4GtcdTAwMDLaytloS8bRXHUwMDE1SSFcYvxcIlZsbug6T/eMvcDvRGX3e9/FhMWxYohcYkTgmo64XHUwMDFjXHUwMDFhtWW3Xe9haGL7flxmmq91u1x1MDAxZv6TNHXogFxufZl8aPCa5zaMo1x1MDAxN2pwUk4wXHUwMDE0XHUwMDAzkVx1MDAwYtRlMKDt1utewlx1MDAxZmuggVxyMoPdLCzCXHUwMDBm3Ibbsb3KkIKpMfmIXHRcdTAwMTOCXHUwMDEyI6KmRaVcIkozJjDOXHUwMDFllKkotahBSYVFXGJFhHCFNNA9OVx1MDAxNJREUPBcdTAwMWNcdTAwMDAnJoFYYIJZflFpSawxVlxcXHRkXFyejFx1MDAwNyUjXHUwMDE2XHUwMDE1lFJcYlxcRVx1MDAxNUpQ0+dcdTAwMTiVmFx1MDAxMcpwwjczx2hf1dfG6Fx1MDAxMJq9IEbDyFx1MDAwZaJ1t1N3O1xy2Fx1MDAxOV/7nnl9lpjoR3GtZ7QsXCJcdTAwMGJcIlxcMiVcdTAwMTUgLONcdTAwMWMmViXGNeyusaOFsWCYXHUwMDFhsFVgcfI0YHBcdTAwMTUuOJ36bK22909Pd13S+7z9cNw624nOTsrlL5O0XHUwMDAypVx1MDAwMDypQDA/iiNcdTAwMDPwMeZcdTAwMGW00lx1MDAxNlKUXHUwMDBiialWiFA5ppRnh1HJb7fdXGKs/9l3O9GolfvmXFwz4d507ProXjip5L5RXFzoXHUwMDFhicNkN/60XHUwMDFhx03/y+DzX1x1MDAxZieOLk51Z/NcdTAwMWFz5FjcSvJ9XHUwMDFholx1MDAwNU4teoznXHSoRijlo5ufUY0gpYngmtLMsJY+y4tcbmuYUEsyXG54QJnJ31hsXHUwMDEyI4FiZmkuIaWD2Vx1MDAxMZrlSDZETPxcdTAwMDZApmKweEIuXHKhylx1MDAxMCM5kIs0Zu2fXHUwMDE3T2+6p9WOOjhu3n1cdTAwMGbsi1wiZ29PL7pF50SSXHLt7jKt9nuHLba9s5eNsKfKPd87u707YFx1MDAxYp9cdTAwMGVcdTAwMDJe334gPcI2xFx1MDAxY+SS8/ruzlar9kmtMVxcaXtHm52vjTnIzcm8yyW2Jc53b7e6J/iqXHUwMDE3Nlu3fFx1MDAwYl1cdTAwMWbW/3XGfUNcdTAwMGX7XHUwMDEzWSHqXu00Nq+0s1x1MDAxOVRxsX5cdTAwMWKG3l4wXHUwMDA3KyCnsvnJ82XjqHm/s394sulT725cdTAwMTGtO6tOMvmAsdhnejCVjDLNgcfH5Dmnelx1MDAwNrDsqSRcdTAwMDNcdTAwMGItXHT8yewkI93OXHUwMDBiSzKwSiVcdTAwMTlcdTAwMTRZ7H1IXHUwMDA2n0AyXHUwMDEySf5cdTAwMTPJgFx1MDAxY85cdTAwMTQxWDwv71DBeHRI/Fx1MDAwMoecXFzByFqxKD2XXHUwMDEzPlxcdsxcdTAwMGW3/uelWVx1MDAwM/H8xmXhsjO5mMHJkJxBrcJzrofd/0WljFx1MDAxOdR5tJQxU/PUSE3NXHQopmRauCpIXyVjQmaO1kP3XHUwMDA2bzU66/dcdTAwMDeoev39SFKvVKr/2GjlM4OVMG0pXHUwMDAyXHUwMDE5L1wiWFxuotVwsDJcdTAwMDU5XHUwMDE505JJzjWwcZFfsGo0XHUwMDFlrEqMXHUwMDA2K1NcdTAwMDIyYczeOSNwTnmVVuAlO8Xt1s3DWsA3rn5lXHUwMDA088pcYnIy73KJzSsjWC4r5JVcdTAwMTEsl1x1MDAxNTy9XlLV7Vx1MDAxMitcdOGUK/v3ja2teTD3nNTNK4FZlkmblb9MPmAs9unTXHUwMDBmz18oYdNbXySmlFx1MDAwYqqzM6J0Oy8sI2LpjEi+XHUwMDE3I1JcdTAwMTNcdTAwMTiRXHUwMDFjY0RKaES1UD91+rLei1wiv/PBbHvMXHUwMDAyLlx1MDAwYlx1MDAxN054Wfj4+O3WXHUwMDBlXFy7XHUwMDEzQWJcdTAwMTD2ajU4u+k5jVx1MDAxY1x1MDAxNj6nnGZcdTAwMDb3XHUwMDFmzWledzqpIT0j0Vx1MDAxMVPjmmjNMKTj2fss1Oej3fXjXHUwMDBi965q22F5u/LlU/PsfvHLXHUwMDEy1IJ4RVxcYoI1XHUwMDExmqmRuMaWwtysWULSQ4XKL64zZjpCaa6QeOe2tWtSo7h3Ujyon8pcdTAwMTLueC1ug9Bfmc6cMp2czLtcXGLzynSWy1xueWU6y2WFvDKd5bJCXis1y2KFWVx01ORcdTAwMDPGYp8+LUBcdTAwMDLFU1x1MDAxMijJJWGSZb93IN3Oi8q01FxmoiXei2hlS6BcdTAwMTjTjCgt/m1cdNShPyHhcFx1MDAwMF2C986eZiRcdTAwMTRcdTAwMTmyp1nnklx1MDAxYcxTO2FcdMJpy7mYMcp59rwpXHUwMDFk5Fx1MDAxNzWaibRcdTAwMTinlEqJXHUwMDA0I5QmXG5ccv1wRtzChGAlsemkppLnXHUwMDE3z1x1MDAxOFtCgFx1MDAxMkZcdTAwMWZcdTAwMDJQy8bDW3CLa66V6ZXUSGI6XHUwMDFh7Vx1MDAwMExCICXly6P99b2wL7/6JPSY1lx1MDAwYlx1MDAxYveRMkw5XHUwMDAxX6RiSmsrsTCYjVx1MDAxMDNCKMVoou8yS/fq0+DZnbCxTmBlXHUwMDBlXHQsolxmfFx1MDAwMsWoO9BcdCZcdTAwMTNpKbRcdTAwMDR9kIBJw9N0mlxmXHUwMDBmYzotUyPsdFc2rzEnjsWtJN9f3tqvp9d3KTiQubsxO56lV/1cdTAwMTdcdTAwMTXPqDbBgFx1MDAxOdicXHUwMDEygHBcdTAwMWRfQ1x1MDAxZvFMWFRcdTAwMGJcYiNNNVx1MDAxNXnecIOpXHUwMDA1XGZIK1x1MDAwNJPNpFBcdTAwMTN6+1x1MDAwNbGEpkhcdTAwMDBfUlx1MDAxMoGyXHR0fWIvXFxcIlx1MDAwZVx1MDAwMPOKXHUwMDA18Vx1MDAwNcWzXCKAXHUwMDA3NVxyOTA5nEgmkmD1iFx1MDAxZGA4XG5mU2ZBQlxinWgxylx0z4xORiWzJoBcdTAwMDG2cNySXHUwMDFjXHUwMDAzXHUwMDFhtYhAYFx1MDAxZkSxUlx1MDAxYatpOk0uXHUwMDE2LzWeXHUwMDE1pzuzeY278Vx1MDAwYlx1MDAxMS21uq3o9LtcYvshTjDJjmqVXHUwMDA3t1k+uHPuSifOt/Jaw1x0i9/2fyyq0VmgRihcdTAwMWVes1x1MDAxYcm4jPmxJNLgXHUwMDE5SvY0veZ2bVZcdTAwMTPONWdsXHUwMDFj0UhcIpmLK9sxTDzfj4SVuWmKklx1MDAxY+5HXHUwMDFh+NWEqkXt+vz4QNb3mq1cdTAwMWLvXHUwMDBivjj8fmJvbb29XHUwMDE4UpJHt7bT27mxT++i8yN6XHUwMDE4XHUwMDFlRvuTxb6odpOTunNcdTAwMTc7q3Yz+YCx2GdcdTAwMDBIudpcdTAwMDDyJlx1MDAxNk1yqt1IOb10g1x1MDAwMdZcdTAwMTBcdTAwMDeyllx1MDAxOUbSzbyoMFwiUmBcdTAwMDRi1sJcdFx1MDAxOHlcdTAwMTOKpDIjwifgXGJcdTAwMTlL5bBkXFzBgX5A4eZV1CdRuCFoaOugcFx1MDAxM5/Kc+HGuTU6WfvOw4eW8/DnZaFyWZhy67FcdTAwMWX68dxuPZ5xQVx1MDAxY63OTFb49Vd3nUjzx8KSYE2xTjzaY1ZYejdl9nnz/qHZ3tpu672d43DXX1/wsFx1MDAwNPSxuGKacHPNXHUwMDE0hI801Fx1MDAxM2KZZmVI/Vx1MDAxOXBmJN90N/LbL+8ms9LoVVx1MDAwYtdvubpfXHR0UdtZP946p3vVs/2Tln/8eWNxr+45qbssYmeRhslcdTAwMDfMqG2vWj/fYdXKzV6RrYn2UbVabspsc5aBjEhNaO5PRlE65W5lSLA4ZSp7TpM+fYuKejxcdTAwMTX1OLbk3FBvXHUwMDFldISYslx1MDAwMEf8XZ+EXHUwMDAy7kg5euuTUJaJjsy4gudNR8wjiaZGJqZKcYNcdTAwMGaZI3N9jbZcdTAwMTTfaWzfn5XOfMlC7yxoLXpcclx1MDAxNZtcdTAwMWGpXHUwMDEwXHUwMDFjklx1MDAwMY20XHUwMDEymlxyhSbm3JJcXHJwTZO64eRcIsyPICRcXDNO2KtcdTAwMTZ430JIPt/qYD3c6V03v29ffN3arGzc3PdcdTAwMTaXkOSk7nKJ3f3aq5xViN495Vx1MDAxN2frd07P35XeXCJcdTAwMWF3XHUwMDE2f5p8wFx1MDAxZs+fKFVEXHUwMDEynHsxXHUwMDA3J59cdTAwMDQ5XG7TXHUwMDE0XHUwMDAwQSRJ3CyUTp++RUVpjFNRWnGLzFxypedSz4G0kSMk87i5c55cdTAwMGW57Fxmalx1MDAwNueYXHUwMDAzg0p5ttz09WdJXHRcdTAwMTOcveDRcqkw9aKoJO9cdTAwMTaV5vZcItPywKnCiGAx0lx1MDAxZEeRtFx1MDAwNMdSIVxykavV9NuLXHUwMDFjIeVbgtI8fkxRhDnlXGaSK8YxmfC4XHUwMDA0XHUwMDAxWVx1MDAxNoP9XHUwMDE0eC2nVNCx9jmTnUlcdTAwMDRcdTAwMTDznlx1MDAxZDVPUfuq/rmMT5dLzzNWh5/jXHUwMDA2zF9rJMCI2DzCLNHN8bw4zCxBuVx1MDAxNppIhVx1MDAxOWj+3NPxwqfLpcfu6tCKNaJEKTgq4qbpR3MxrpayXHUwMDAwdDVkrlxcKUSVYGNaLdUqdJpP91x1MDAwN4y7cyxzJfn+Yr6RuJyNXHUwMDAwXHUwMDFiU5pcIk1Zdrpx4GyfXpVON4vnVcq/ke1cdTAwMTLZrp0sPLBJYVx1MDAxMbP2jzhcdTAwMDZGQYZxjUjz5DlOzJ2KXG6ihOXXJpgowMRkY6xvxviAIFi9a7VcdTAwMDbYXHUwMDE45TSvxaPxrt9qr1pNXHUwMDEygSS5UEOjs3byRn53XHUwMDFhr1x1MDAxODqLUVx1MDAxMvGkyWNsrTxFcMHudstcdTAwMTFYaFx1MDAwMHgwXHRu/ek0Y3mFW9e5W59QXHUwMDFhuO6/jNR+vJrIcMxcdTAwMTT8/c/KP/9cdTAwMDeoXHUwMDAzMlx1MDAwNiJ9 App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

    The App class is always the root of the DOM, so there is nowhere for the event to bubble to.

    "},{"location":"guide/events/#stopping-bubbling","title":"Stopping bubbling","text":"

    Event handlers may stop this bubble behavior by calling the stop() method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance when a text input widget responds to a key event it stops the bubbling so that the key doesn't also invoke a key binding.

    "},{"location":"guide/events/#custom-messages","title":"Custom messages","text":"

    You can create custom messages for your application that may be used in the same way as events (recall that events are simply messages reserved for use by Textual).

    The most common reason to do this is if you are building a custom widget and you need to inform a parent widget about a state change.

    Let's look at an example which defines a custom message. The following example creates color buttons which\u2014when clicked\u2014send a custom message.

    custom01.pyOutput custom01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.message import Message\nfrom textual.widgets import Static\n\n\nclass ColorButton(Static):\n    \"\"\"A color button.\"\"\"\n\n    class Selected(Message):\n        \"\"\"Color selected message.\"\"\"\n\n        def __init__(self, color: Color) -> None:\n            self.color = color\n            super().__init__()\n\n    def __init__(self, color: Color) -> None:\n        self.color = color\n        super().__init__()\n\n    def on_mount(self) -> None:\n        self.styles.margin = (1, 2)\n        self.styles.content_align = (\"center\", \"middle\")\n        self.styles.background = Color.parse(\"#ffffff33\")\n        self.styles.border = (\"tall\", self.color)\n\n    def on_click(self) -> None:\n        # The post_message method sends an event to be handled in the DOM\n        self.post_message(self.Selected(self.color))\n\n    def render(self) -> str:\n        return str(self.color)\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        yield ColorButton(Color.parse(\"#008080\"))\n        yield ColorButton(Color.parse(\"#808000\"))\n        yield ColorButton(Color.parse(\"#E9967A\"))\n        yield ColorButton(Color.parse(\"#121212\"))\n\n    def on_color_button_selected(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    ColorApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(0,\u00a0128,\u00a0128)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(128,\u00a0128,\u00a00)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(233,\u00a0150,\u00a0122)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(18,\u00a018,\u00a018)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the custom message class which extends Message. The constructor stores a color object which handler methods will be able to inspect.

    The message class is defined within the widget class itself. This is not strictly required but recommended, for these reasons:

    • It reduces the amount of imports. If you import ColorButton, you have access to the message class via ColorButton.Selected.
    • It creates a namespace for the handler. So rather than on_selected, the handler name becomes on_color_button_selected. This makes it less likely that your chosen name will clash with another message.
    "},{"location":"guide/events/#sending-messages","title":"Sending messages","text":"

    To send a message call the post_message() method. This will place a message on the widget's message queue and run any message handlers.

    It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a on_color_button_selected if it wanted to handle the message itself.

    "},{"location":"guide/events/#preventing-messages","title":"Preventing messages","text":"

    You can temporarily disable posting of messages of a particular type by calling prevent, which returns a context manager (used with Python's with keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed.

    The following example will play the terminal bell as you type. It does this by handling Input.Changed and calling bell(). There is a Clear button which sets the input's value to an empty string. This would normally also result in a Input.Changed event being sent (and the bell playing). Since we don't want the button to make a sound, the assignment to value is wrapped within a prevent context manager.

    Tip

    In reality, playing the terminal bell as you type would be very irritating -- we don't recommend it!

    prevent.pyOutput prevent.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Input\n\n\nclass PreventApp(App):\n    \"\"\"Demonstrates `prevent` context manager.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Input()\n        yield Button(\"Clear\", id=\"clear\")\n\n    def on_button_pressed(self) -> None:\n        \"\"\"Clear the text input.\"\"\"\n        input = self.query_one(Input)\n        with input.prevent(Input.Changed):  # (1)!\n            input.value = \"\"\n\n    def on_input_changed(self) -> None:\n        \"\"\"Called as the user types.\"\"\"\n        self.bell()  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = PreventApp()\n    app.run()\n
    1. Clear the input without sending an Input.Changed event.
    2. Plays the terminal sound when typing.

    PreventApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Clear \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/events/#message-handlers","title":"Message handlers","text":"

    Most of the logic in a Textual app will be written in message handlers. Let's explore handlers in more detail.

    "},{"location":"guide/events/#handler-naming","title":"Handler naming","text":"

    Textual uses the following scheme to map messages classes on to a Python method.

    • Start with \"on_\".
    • Add the message's namespace (if any) converted from CamelCase to snake_case plus an underscore \"_\".
    • Add the name of the class converted from CamelCase to snake_case.
    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa0/byFx1MDAxYf7eX1x1MDAxMeV8OStcdTAwMTV37pdKq1x1MDAxNbC0hZSKXHUwMDE2SludripjT1x1MDAxMlx1MDAxN8c29oTLVvz3845cdLGdXHUwMDFiJFx1MDAwNJaNXHUwMDA0iWcm9ut3nud5L86vXHUwMDE3rVbbXmWm/brVNpeBXHUwMDFmR2HuX7RfuvFzk1x1MDAxN1GawFx1MDAxNCmPi3SYXHUwMDA35cq+tVnx+tWrgZ+fXHUwMDFhm8V+YLzzqFx1MDAxOPpxYYdhlHpBOnhcdTAwMTVZMyj+cP8/+Fx1MDAwM/N7llx1MDAwZUKbe9VFNkxcdTAwMTjZNL+5lonNwCS2gLP/XHUwMDBmjlutX+X/mnW5XHSsn/RiU36hnKpcZqSMT45+SJPSWEyEIEghQcYrouJPuJ41IUx3wWZTzbih9uHRWXbGf36Sxu/wY3HOWYfH1WW7UVx1MDAxY1x1MDAxZtqruDSrSOFuqrnC5ump+Vx1MDAxMoW27649MT7+VuhcdTAwMTd9U/tanlx1MDAwZXv9xFx1MDAxNO7+0Xg0zfwgslfuRKhcdTAwMWG9cUJ93WXpXHUwMDAx5mkqJEOSY0mVqlx1MDAxY+JOQFx1MDAxNfVcdTAwMTRiWlx1MDAxMy2xkpLwXHTTttNcdTAwMTj2XHUwMDAyTPtcdTAwMGYqX5VtJ35w2lx1MDAwM1x1MDAwM5OwWtP1OeHg2GrVxeiWufaI1FIqxqjQRHAxXtI3Ua9vYVxymMqphlx1MDAxZKlcdTAwMTlhyt2QREmMqVx1MDAxYY+7XHUwMDBiZ7thiYu/Jr3Z9/Ns5LR2aWDNaHe4U1x1MDAwM1X15WFcdTAwMTb6N3uPhaCMYiYwl3Q8XHUwMDFmR8kpTCbDOK7G0uC0gks5ev1yXHUwMDA1nHIh5uGUaoKpVETcXHUwMDFiprvKbH3K2LtT8/VL9mZjiFx1MDAwZnW8/8xhypDwsFx1MDAwMjcgXHUwMDAxb0hKOVx1MDAwMVPmwYZIqplmgmPxIJRicqKUmIVSgpCHXHUwMDA1lkpywrRAWC2DUsw1sEgyjNaP09FEXHUwMDA1rNqGZ2pPXHUwMDA1vv3z8it786M3/FZcdTAwMWO9XHUwMDFkRuNzNVDo53l60Vx1MDAxZc9cXI8+zWdcdTAwMDHilMHNPlx0XHUwMDBiXHUwMDE0V/NYoCRBVFx1MDAwYszuzYKtg86eke92XHUwMDBlXHUwMDA2W+Kk0z00XHUwMDE355vZM2eBQMqTXHUwMDFjIaEoXHUwMDA1KFwiPEVcdTAwMDLFXGIjXHUwMDFhlNyJwsNYwFx1MDAwMmG6fFx1MDAxNlx1MDAwYjDhXHUwMDFlgFx1MDAxOPM6xu+Bf65cdTAwMTQhgj6GTC+C//bnz2FHXuxcdTAwMDdcdTAwMWb3tnl+2T/svDtGa4M/k6qGulx1MDAwN8Lfmks7XHUwMDBi+Vx1MDAxOOm5XHUwMDAxXHUwMDAwXHUwMDEyXHUwMDE1XGKcXGYtgX1fXHUwMDFl51x1MDAwN/tZxFx1MDAwZtOf4uJIbnx7n+C1Yn/iW3Xo45WgT1x1MDAxMfVcdTAwMTjSWiHIWIRqyj+XIMtcdTAwMWMxqTlnXHUwMDFhXHUwMDA06bGAr+g03jmaxLnETElIY8TyOC/cwYo4XHUwMDE3Nk63szOqTj7GfPPqfNOc7H5dXHUwMDEzzlx0oYIqslx1MDAwNM4rNKWJPYz+NmX4bIy+8Vx1MDAwN1F81YBESVx1MDAwMDBw3z81Rcv2TWtgbD9ccr8nPnwqXG6/Z1p9P1x0Y5PXN7EwYJC7XHUwMDAyo41TbcZRz/GnXHUwMDFkm26TWDaCemI8bdOa01x1MDAwMzDNh9Plu+HkLaZ51ItcdTAwMTI/PlrCzJVcYi/lXFy+Q6AjIFx1MDAwN7yWRdzF9zefd97KLOb9y1x1MDAxZPb+7eCg8377p37efGeMe1x1MDAwNFx1MDAwM6khmDHBVDPUQcrhQSjhXHUwMDA0SSYg4j1apNOVqC4gPKFgiUtcdTAwMGKflvCPmddcdTAwMTFcdTAwMDI7oKpcdTAwMWJ6dMKPWJNAzV9cdTAwMDBOzPfkv+nQmrxcdTAwMTXEflH8tlx1MDAxNNtcdTAwMDPwXl0g1sf3u6xcXInsXHUwMDAyycnRMdlcdTAwMTXVUNjo+3M97n56XHUwMDFmn3X2985+fOufpztcdTAwMWaO9o7X24RYO9dcdTAwMDUlnlx1MDAwMFx1MDAxYbuyTihBmlxc54J6RFx1MDAxMq2g2Fx1MDAwNtFTXHUwMDBm60As4DqrrruA61hLXHJ/XHUwMDFjVcHwScj+mFksRHdcdTAwMDVC+2Rkd429Vtr9ntzGypI9z4Pic2xbSOxcdTAwMWKHz6pYiZ5cdTAwMWO9ZTaTpKyH7t9eXFyc3y3BbDKJ0Vx1MDAxNZkt70zahfa05sQ1ypieiOGcS09Csq6ohDBcdTAwMGbEnsvrUDOFuqtXq641XHUwMDA0nlaUKC2FnNFZxIR6XHUwMDFhMSpcdTAwMTVcdTAwMThcdTAwMDJcdTAwMDU0q1x1MDAwNPm2duVQ5nGxXHUwMDAy6VfvMN4k3ct0XHUwMDE4a3b4ud2KkjBKejBZ6cltx3z3XHUwMDFlhWBJ5GDorNxAXHUwMDFlRYxzjClcdTAwMTJQ84Jtsras52cjT3NcdTAwMDFcdTAwMWJcdTAwMGVBS1x1MDAxM4bxaMH12CyThHdcdTAwMWLV/Vx1MDAxNvTzqyP68biTXHUwMDExxTa2gy8mnmVcdTAwMTTyXHUwMDE0XHUwMDE4xFx1MDAxMFx1MDAxNH1cdTAwMTIkWVA9bVx1MDAxMvdcdTAwMTBsLFx1MDAxM6497HrYesomoLfdTlx1MDAwN4PIgu9cdTAwMGbSKLGTPi6duek43jf+lIDAPdXnJsUgc2dsqnv1qVVcdTAwMTGmPFx1MDAxOH/+6+XM1fOx7F5cdTAwMWLTMK5O+KL+vrSQQVx1MDAxMEeTw5WSXHTYdCnv339YnLg+RyXjWHuuv4ggzVx1MDAwN6+z6sJlOaKQx6FcIlx1MDAwM4oghvWChyQ80FxmhatKXHUwMDE5XHUwMDA1XHUwMDFiXHUwMDE4p5hcbsY10bXYUXXfqMcoXHUwMDE0RIpgxFx1MDAxMVx1MDAxMzVVXHUwMDFk5S+YKIFcdTAwMTnI8dNKXHUwMDE5g1x1MDAwZlUwXFy/lC2ucZtS5kpoTDjDXG70SkheY9FINzQkpJhqXHUwMDA0rlx1MDAwNDditpqSLX7S0rRcdEmCXHUwMDA1lLRKYoqRXHUwMDE0fMomXHUwMDA1abBEXHUwMDAyYVxydkGWPG3Uv0nKNuaCuZydwvHalIyrudVcdTAwMTZWIJ5Y1blxl5QtTsv/XHUwMDAxKVN3VltaeKrsW0OgRnDHXHUwMDEzWZlcdTAwMDAoUijFXHUwMDFj7sX8xlxubFs3kKsqXHUwMDE5cTVcdTAwMWRkN1x1MDAxYVx1MDAwNFWDINVcdTAwMTKuKinDwnNSi7iCilx1MDAwYik5lZRp95iB1DvfT5OVrVos3VPKXHUwMDE2l/CNXHUwMDA0XGLCsqZcXFNcbuVcdTAwMDSkZOCQXHUwMDFhjUa6IT1QXGbwn1x1MDAwMGAzwlx1MDAxOF1NzFx1MDAxNj8wa1olmGBaXHUwMDBipIWSkFxyztJXXHL7XHUwMDBmlTSDRIVCMf3v1rL5cC6np5G8pJrN61x1MDAxY1x1MDAxMTz3iSiBbFx1MDAwNMKJXFyidbQ48W5qWd9cdTAwMGb6w9zMU7N1NY/0nSUmV56mXHUwMDAwJii1QdpcdTAwMTVrylx1MDAxOVXSQ1gypLSm5IE/XGawuZ9cdTAwMTSZn1x1MDAwMyVm5GZcdTAwMTJcIlx1MDAxNilcdTAwMDFfnmlGbkaxJ1x1MDAxNSbAjNGrZs2oyoRbgJJ4lVx1MDAxZlxiPNcnR5hJcEu1tyv2loTHXHUwMDE0XHUwMDE0Nje+hZdsrFx1MDAxYfeamt1cImdwmvxcYtxcdTAwMDb+OFx1MDAxOVpcdTAwMGJcdTAwMDdcdTAwMDVQILCmkYOPu02Ez92hp3icNNfWXHUwMDE3t74u/dz2s+zQgpfHalxyMInCkauqK7TPI3OxNetnWOXLnbVcdTAwMTRcdTAwMWPHbONA8uv6xfX/XHUwMDAx2ibQXHUwMDAzIn0= Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected

    Messages have a namespace if they are defined as a child class of a Widget. The namespace is the name of the parent class. For instance, the builtin Input class defines it's Changed message as follow:

    class Input(Widget):\n    ...\n    class Changed(Message):\n        \"\"\"Posted when the value changes.\"\"\"\n        ...\n

    Because Changed is a child class of Input, its namespace will be \"input\" (and the handler name will be on_input_changed). This allows you to have similarly named events, without clashing event handler names.

    Tip

    If you are ever in doubt about what the handler name should be for a given event, print the handler_name class variable for your event class.

    Here's how you would check the handler name for the Input.Changed event:

    >>> from textual.widgets import Input\n>>> Input.Changed.handler_name\n'on_input_changed'\n
    "},{"location":"guide/events/#on-decorator","title":"On decorator","text":"

    In addition to the naming convention, message handlers may be created with the on decorator, which turns a method into a handler for the given message or event.

    For instance, the two methods declared below are equivalent:

    @on(Button.Pressed)\ndef handle_button_pressed(self):\n    ...\n\ndef on_button_pressed(self):\n    ...\n

    While this allows you to name your method handlers anything you want, the main advantage of the decorator approach over the naming convention is that you can specify which widget(s) you want to handle messages for.

    Let's first explore where this can be useful. In the following example we have three buttons, each of which does something different; one plays the bell, one toggles dark mode, and the other quits the app.

    on_decorator01.pyOutput on_decorator01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n        \"\"\"Handle all button pressed events.\"\"\"\n        if event.button.id == \"bell\":\n            self.bell()\n        elif event.button.has_class(\"toggle\", \"dark\"):\n            self.dark = not self.dark\n        elif event.button.id == \"quit\":\n            self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    1. The message handler is called when any button is pressed

    OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Note how the message handler has a chained if statement to match the action to the button. While this works just fine, it can be a little hard to follow when the number of buttons grows.

    The on decorator takes a CSS selector in addition to the event type which will be used to select which controls the handler should work with. We can use this to write a handler per control rather than manage them all in a single handler.

    The following example uses the decorator approach to write individual message handlers for each of the three buttons:

    on_decorator02.pyOutput on_decorator02.py
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\n\nclass OnDecoratorApp(App):\n    CSS_PATH = \"on_decorator.tcss\"\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Three buttons.\"\"\"\n        yield Button(\"Bell\", id=\"bell\")\n        yield Button(\"Toggle dark\", classes=\"toggle dark\")\n        yield Button(\"Quit\", id=\"quit\")\n\n    @on(Button.Pressed, \"#bell\")  # (1)!\n    def play_bell(self):\n        \"\"\"Called when the bell button is pressed.\"\"\"\n        self.bell()\n\n    @on(Button.Pressed, \".toggle.dark\")  # (2)!\n    def toggle_dark(self):\n        \"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\n        self.dark = not self.dark\n\n    @on(Button.Pressed, \"#quit\")  # (3)!\n    def quit(self):\n        \"\"\"Called when the quit button is pressed.\"\"\"\n        self.exit()\n\n\nif __name__ == \"__main__\":\n    app = OnDecoratorApp()\n    app.run()\n
    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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 BellToggle\u00a0darkQuit \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    While there are a few more lines of code, it is clearer what will happen when you click any given button.

    Note that the decorator requires that the message class has a control property which should return the widget associated with the message. Messages from builtin controls will have this attribute, but you may need to add a control property to any custom messages you write.

    Note

    If multiple decorated handlers match the message, then they will all be called in the order they are defined.

    The naming convention handler will be called after any decorated handlers.

    "},{"location":"guide/events/#applying-css-selectors-to-arbitrary-attributes","title":"Applying CSS selectors to arbitrary attributes","text":"

    The on decorator also accepts selectors as keyword arguments that may be used to match other attributes in a Message, provided those attributes are in Message.ALLOW_SELECTOR_MATCH.

    The snippet below shows how to match the message TabbedContent.TabActivated only when the tab with id home was activated:

    @on(TabbedContent.TabActivated, tab=\"#home\")\ndef home_tab(self) -> None:\n    self.log(\"Switched back to home tab.\")\n    ...\n
    "},{"location":"guide/events/#handler-arguments","title":"Handler arguments","text":"

    Message handler methods can be written with or without a positional argument. If you add a positional argument, Textual will call the handler with the event object. The following handler (taken from custom01.py above) contains a message parameter. The body of the code makes use of the message to set a preset color.

        def on_color_button_selected(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n

    A similar handler can be written using the decorator on:

        @on(ColorButton.Selected)\n    def animate_background_color(self, message: ColorButton.Selected) -> None:\n        self.screen.styles.animate(\"background\", message.color, duration=0.5)\n

    If the body of your handler doesn't require any information in the message you can omit it from the method signature. If we just want to play a bell noise when the button is clicked, we could write our handler like this:

        def on_color_button_selected(self) -> None:\n        self.app.bell()\n

    This pattern is a convenience that saves writing out a parameter that may not be used.

    "},{"location":"guide/events/#async-handlers","title":"Async handlers","text":"

    Message handlers may be coroutines. If you prefix your handlers with the async keyword, Textual will await them. This lets your handler use the await keyword for asynchronous APIs.

    If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long as handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use.

    Info

    To re-use the chef analogy, if an order comes in for beef wellington (which takes a while to cook), orders may start to pile up and customers may have to wait for their meal. The solution would be to have another chef work on the wellington while the first chef picks up new orders.

    Network access is a common cause of slow handlers. If you try to retrieve a file from the internet, the message handler may take anything up to a few seconds to return, which would prevent the widget or app from updating during that time. The solution is to launch a new asyncio task to do the network task in the background.

    Let's look at an example which looks up word definitions from an api as you type.

    Note

    You will need to install httpx with pip install httpx to run this example.

    dictionary.pydictionary.tcssOutput dictionary.py
    import asyncio\n\ntry:\n    import httpx\nexcept ImportError:\n    raise ImportError(\"Please install httpx with 'pip install httpx' \")\n\nfrom rich.json import JSON\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass DictionaryApp(App):\n    \"\"\"Searches a dictionary API as-you-type.\"\"\"\n\n    CSS_PATH = \"dictionary.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Search for a word\")\n        yield VerticalScroll(Static(id=\"results\"), id=\"results-container\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"A coroutine to handle a text changed message.\"\"\"\n        if message.value:\n            # Look up the word in the background\n            asyncio.create_task(self.lookup_word(message.value))\n        else:\n            # Clear the results\n            self.query_one(\"#results\", Static).update()\n\n    async def lookup_word(self, word: str) -> None:\n        \"\"\"Looks up a word.\"\"\"\n        url = f\"https://api.dictionaryapi.dev/api/v2/entries/en/{word}\"\n        async with httpx.AsyncClient() as client:\n            results = (await client.get(url)).text\n\n        if word == self.query_one(Input).value:\n            self.query_one(\"#results\", Static).update(JSON(results))\n\n\nif __name__ == \"__main__\":\n    app = DictionaryApp()\n    app.run()\n
    dictionary.tcss
    Screen {\n    background: $panel;\n}\n\nInput {\n    dock: top;\n    width: 100%;\n    height: 1;\n    padding: 0 1;\n    margin: 1 1 0 1;\n}\n\n#results {\n    width: auto;\n    min-height: 100%;\n}\n\n#results-container {\n    background: $background 50%;\n    overflow: auto;\n    margin: 1 2;\n    height: 100%;\n}\n

    DictionaryApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the highlighted line in the above code which calls asyncio.create_task to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

    "},{"location":"guide/input/","title":"Input","text":"

    This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.

    Quote

    More Input!

    \u2014 Johnny Five

    "},{"location":"guide/input/#keyboard-input","title":"Keyboard input","text":"

    The most fundamental way to receive input is via Key events which are sent to your app when the user presses a key. Let's write an app to show key events as you type.

    key01.pyOutput key01.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n\n    def on_key(self, event: events.Key) -> None:\n        self.query_one(RichLog).write(event)\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    InputApp Key(key='T',\u00a0character='T',\u00a0name='upper_t',\u00a0is_printable=True) Key(key='e',\u00a0character='e',\u00a0name='e',\u00a0is_printable=True) Key(key='x',\u00a0character='x',\u00a0name='x',\u00a0is_printable=True) Key(key='t',\u00a0character='t',\u00a0name='t',\u00a0is_printable=True) Key(key='u',\u00a0character='u',\u00a0name='u',\u00a0is_printable=True) Key(key='a',\u00a0character='a',\u00a0name='a',\u00a0is_printable=True) Key(key='l',\u00a0character='l',\u00a0name='l',\u00a0is_printable=True) Key( key='exclamation_mark', character='!', name='exclamation_mark', is_printable=True )

    When you press a key, the app will receive the event and write it to a RichLog widget. Try pressing a few keys to see what happens.

    Tip

    For a more feature rich version of this example, run textual keys from the command line.

    "},{"location":"guide/input/#key-event","title":"Key Event","text":"

    The key event contains the following attributes which your app can use to know how to respond.

    "},{"location":"guide/input/#key","title":"key","text":"

    The key attribute is a string which identifies the key that was pressed. The value of key will be a single character for letters and numbers, or a longer identifier for other keys.

    Some keys may be combined with the Shift key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the key attribute will be prefixed with shift+. For example, Shift+Home will produce an event with key=\"shift+home\".

    Many keys can also be combined with Ctrl which will prefix the key with ctrl+. For instance, Ctrl+P will produce an event with key=\"ctrl+p\".

    Warning

    Not all keys combinations are supported in terminals and some keys may be intercepted by your OS. If in doubt, run textual keys from the command line.

    "},{"location":"guide/input/#character","title":"character","text":"

    If the key has an associated printable character, then character will contain a string with a single Unicode character. If there is no printable character for the key (such as for function keys) then character will be None.

    For example the P key will produce character=\"p\" but F2 will produce character=None.

    "},{"location":"guide/input/#name","title":"name","text":"

    The name attribute is similar to key but, unlike key, is guaranteed to be valid within a Python function name. Textual derives name from the key attribute by lower casing it and replacing + with _. Upper case letters are prefixed with upper_ to distinguish them from lower case names.

    For example, Ctrl+P produces name=\"ctrl_p\" and Shift+P produces name=\"upper_p\".

    "},{"location":"guide/input/#is_printable","title":"is_printable","text":"

    The is_printable attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If is_printable is False then the key is a control code or function key that you wouldn't expect to produce anything in an input.

    "},{"location":"guide/input/#aliases","title":"aliases","text":"

    Some keys or combinations of keys can produce the same event. For instance, the Tab key is indistinguishable from Ctrl+I in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of Tab, the aliases attribute will contain [\"tab\", \"ctrl+i\"]

    "},{"location":"guide/input/#key-methods","title":"Key methods","text":"

    Textual offers a convenient way of handling specific keys. If you create a method beginning with key_ followed by the key name (the event's name attribute), then that method will be called in response to the key press.

    Let's add a key method to the example code.

    key02.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n\n    def on_key(self, event: events.Key) -> None:\n        self.query_one(RichLog).write(event)\n\n    def key_space(self) -> None:\n        self.bell()\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    Note the addition of a key_space method which is called in response to the space key, and plays the terminal bell noise.

    Note

    Consider key methods to be a convenience for experimenting with Textual features. In nearly all cases, key bindings and actions are preferable.

    "},{"location":"guide/input/#input-focus","title":"Input focus","text":"

    Only a single widget may receive key events at a time. The widget which is actively receiving key events is said to have input focus.

    The following example shows how focus works in practice.

    key03.pykey03.tcssOutput key03.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\n\nclass KeyLogger(RichLog):\n    def on_key(self, event: events.Key) -> None:\n        self.write(event)\n\n\nclass InputApp(App):\n    \"\"\"App to display key events.\"\"\"\n\n    CSS_PATH = \"key03.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield KeyLogger()\n        yield KeyLogger()\n        yield KeyLogger()\n        yield KeyLogger()\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n
    key03.tcss
    Screen {\n    layout: grid;\n    grid-size: 2 2;\n    grid-columns: 1fr;\n}\n\nKeyLogger {\n    border: blank;\n}\n\nKeyLogger:hover {\n    border: wide $secondary;\n}\n\nKeyLogger:focus {\n    border: wide $accent;\n}\n

    InputApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 name='o',\u258echaracter='d',\u258a is_printable=True\u258ename='d',\u258a )\u258eis_printable=True\u258a Key(\u258e)\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u2586\u2586\u258ename='exclamation_mark',\u2587\u2587\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\u258a )\u258e)\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    The app splits the screen in to quarters, with a RichLog widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.

    Tip

    the :focus CSS pseudo-selector can be used to apply a style to the focused widget.

    You can move focus by pressing the Tab key to focus the next widget. Pressing Shift+Tab moves the focus in the opposite direction.

    "},{"location":"guide/input/#controlling-focus","title":"Controlling focus","text":"

    Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's focus() method.

    "},{"location":"guide/input/#focus-events","title":"Focus events","text":"

    When a widget receives focus, it is sent a Focus event. When a widget loses focus it is sent a Blur event.

    "},{"location":"guide/input/#bindings","title":"Bindings","text":"

    Keys may be associated with actions for a given widget. This association is known as a key binding.

    To create bindings, add a BINDINGS class variable to your app or widget. This should be a list of tuples of three strings. The first value is the key, the second is the action, the third value is a short human readable description.

    The following example binds the keys R, G, and B to an action which adds a bar widget to the screen.

    binding01.pybinding01.tcssOutput binding01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Footer, Static\n\n\nclass Bar(Static):\n    pass\n\n\nclass BindingApp(App):\n    CSS_PATH = \"binding01.tcss\"\n    BINDINGS = [\n        (\"r\", \"add_bar('red')\", \"Add Red\"),\n        (\"g\", \"add_bar('green')\", \"Add Green\"),\n        (\"b\", \"add_bar('blue')\", \"Add Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Footer()\n\n    def action_add_bar(self, color: str) -> None:\n        bar = Bar(color)\n        bar.styles.background = Color.parse(color).with_alpha(0.5)\n        self.mount(bar)\n        self.call_after_refresh(self.screen.scroll_end, animate=False)\n\n\nif __name__ == \"__main__\":\n    app = BindingApp()\n    app.run()\n
    binding01.tcss
    Bar {\n    height: 5;\n    content-align: center middle;\n    text-style: bold;\n    margin: 1 2;\n    color: $text;\n}\n

    BindingApp red\u2582\u2582 green blue blue \u00a0R\u00a0\u00a0Add\u00a0Red\u00a0\u00a0G\u00a0\u00a0Add\u00a0Green\u00a0\u00a0B\u00a0\u00a0Add\u00a0Blue\u00a0

    Note how the footer displays bindings and makes them clickable.

    Tip

    Multiple keys can be bound to a single action by comma-separating them. For example, (\"r,t\", \"add_bar('red')\", \"Add Red\") means both R and T are bound to add_bar('red').

    "},{"location":"guide/input/#binding-class","title":"Binding class","text":"

    The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a Binding instance which exposes a few more options.

    "},{"location":"guide/input/#priority-bindings","title":"Priority bindings","text":"

    Individual bindings may be marked as a priority, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget.

    You can create priority key bindings by setting priority=True on the Binding object. Textual uses this feature to add a default binding for Ctrl+C so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority:

        BINDINGS = [\n        Binding(\"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True),\n        Binding(\"tab\", \"focus_next\", \"Focus Next\", show=False),\n        Binding(\"shift+tab\", \"focus_previous\", \"Focus Previous\", show=False),\n    ]\n
    "},{"location":"guide/input/#show-bindings","title":"Show bindings","text":"

    The footer widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set show=False. The default bindings on App do this so that the standard Ctrl+C, Tab and Shift+Tab bindings don't typically appear in the footer.

    "},{"location":"guide/input/#mouse-input","title":"Mouse Input","text":"

    Textual will send events in response to mouse movement and mouse clicks. These events contain the coordinates of the mouse cursor relative to the terminal or widget.

    Information

    The trackpad (and possibly other pointer devices) are treated the same as the mouse in terminals.

    Terminal coordinates are given by a pair values named x and y. The X coordinate is an offset in characters, extending from the left to the right of the screen. The Y coordinate is an offset in lines, extending from the top of the screen to the bottom.

    Coordinates may be relative to the screen, so (0, 0) would be the top left of the screen. Coordinates may also be relative to a widget, where (0, 0) would be the top left of the widget itself.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ba0/bSFx1MDAxNP3eX1x1MDAxMaVfdqUynfej0mpcdTAwMDVcdTAwMDGaQHktXHUwMDAxXG6rVeUmTmxw7GA7XHUwMDA0qPjve+1A7DxcdKHJhlXzXHUwMDAxyIxcdTAwMWbXM+ecOfeO+fGuUCjGd227+KlQtG9rlufWQ6tb/JC039hh5Fx1MDAwNj500fR7XHUwMDE0dMJaeqRcdTAwMTPH7ejTx48tK7yy47Zn1Wx040ZcdTAwMWTLi+JO3VxyUC1ofXRju1x1MDAxNf2Z/Ny3WvZcdTAwMWbtoFWPQ5TdZM2uu3FcdTAwMTD27mV7dsv241xirv43fC9cdTAwMTR+pD9z0YV2Lbb8pmenJ6RdWYBcdTAwMDRcdTAwMTM63LxcdTAwMWb4abREcIO1xFxc9I9wo024YWzXobtcdTAwMDFB21lP0lRcXN/ddcqh3GlFe7dcdTAwMDd1p4nXP5Nqdt+G63nH8Z2XxlVcdTAwMGKDKFpzrLjmZEdEcVx1MDAxOFxc2WduPXaehi/X3j83XG5gKLKzwqDTdHw7SkaB9FuDtlVz47v0KXG/tTdcdTAwMTSfXG5Zyy18Y1xcIS1cdTAwMTVcdTAwMTFEsuShs0dOzqdKIK2l4ZphTnVuQHpxlVx1MDAwMlx1MDAwZuZcdTAwMDPieo/TT1x1MDAxNtl3q3bVhPD8ev+YOLT8qG2FMGvZcd3HJ1x1MDAxNpwjySjjXGZcdTAwMGJCSHYjx3abTpxEKjHSXHUwMDA0XHUwMDFiziVjgmgqs2DsdGKMVIJjzni/I4mgXamnIPlneExcdTAwMWQrbD+OXTFKvuSiT1x1MDAwMt/KISw7udOuWz1cdTAwMWNcdTAwMTApOVFKJSGpfr/n+lfQ6Xc8L2tcdTAwMGJqV1x1MDAxOXTS1odcdTAwMGZzgFZRPFx0s1RcdTAwMWKYKMrUzJC9pXW+tS3Pr02zVbnc2zrnNz6ZXHUwMDAw2SHY/ZdgXHUwMDE1XHUwMDE4XHUwMDBiLFx1MDAxODbKaDlcdTAwMDJWhinBXG5LSoyii0QrQ5hIQbVQRFx1MDAxOaVH4Uo1klx1MDAxMjNcdTAwMGU6QiVcdTAwMDNoj8BVSmNcYjVcdTAwMDS/Ybjanue2o7FglUJMXHUwMDAyq1x1MDAxMdhcYi3MzFg9XzPfhFPdPmp2tXdcdTAwMTFEe/5e4/M8WCXLw6owiFx1MDAxOclcdTAwMTnAQ1x1MDAxYWL0IFa1QEJcdTAwMDEoXHUwMDE4pVx1MDAxOGuB+Wuw+r5hXHQq6ChOXHRDXHUwMDFjUyqMpPCLa81HgUooXHUwMDEyXHUwMDAwXHJcdTAwMDOaiiljNFx1MDAwN45HoFxuiFx1MDAxNEiVQ/D/XG6ocKOJToBcdTAwMTgjYU0hbGaofnXx93XiXHUwMDFj8Z3908PWXHUwMDA2/evM7norXHUwMDBlVS1cdTAwMTGjilx1MDAxOSap1ppcdTAwMTA1hFWOXHUwMDAwXHUwMDFjSmiDXHUwMDA11yQnu/Nh9TtI+KKwSlx1MDAxOSxcZkyI/6moKskmYlx1MDAxNVx1MDAxNlx1MDAxYWpcdTAwMTjFs2M12rwpy3W3s3dcdTAwMWWas9qFiNbL5Hi1scpJXHUwMDAyRlx1MDAwNoNONFx1MDAwMJaTIagypDBcdTAwMDYzZFx1MDAxOKgue5VcdTAwMDN4T+h38FSLQiphXHSjXHUwMDE4e8tItcIw6I5Nr9jExZ8rpjCTuVx1MDAwNfE5mKqD+0atpiW/3qVhyexcdTAwMTFIXHUwMDAzK1x1MDAxM2DqWDWnXHUwMDEz2v89UJlUSEkh6WBGxVx1MDAxOEFcdTAwMTgyLblAd4pcdTAwMTFVXG4yKTUmi5JitPNcdJDwUMJoMUf6lFx1MDAwNjcnILlcdTAwMDBcdTAwMWL9XHUwMDAyQObisMJ4w/Xrrt9cdTAwMWM+xfbrWc9cdTAwMTNsXHUwMDBi/apBpecqO9s7+5ub3Vx1MDAxM6dy24lOon1cdTAwMTmfZrhKkFx1MDAxNdQ6UTqghFx1MDAxOUHBrFx1MDAwM+UlOFx1MDAwMpI7qGm1XHUwMDEzVCNB01F97HjIoreiuFx1MDAxNLRablxmz31cdTAwMTi4fjxcdTAwMWNs+iDrXHSVXHUwMDFj26qPeZR83zDn2slcdTAwMTWzKkjyyf4qZKBMv/T//ufD2KPXRqGTfHKgya7wLv/7xVx1MDAwMqHkcGM/k4XEXG6QSNTsXHUwMDAyXHUwMDEx3G59bVxcnljd06tSuXFz0vWv/7pYfYGAXGZcdTAwMTFWMTUkXHUwMDEw1CDJsVx1MDAwNpVkXHUwMDFhw2rOhlwi+olZrEGQepixOkFcdTAwMTDBZEC9nlRCc2a4WrZMXHUwMDE4pvNcdP0yZeLwRl+FR+t39dYhO7mp4jiu7tTHy1x1MDAwNCZcdTAwMTTUjIO6q0RLiaa5w3pCQTCSvZF900oxip3ks9aHzVx1MDAwYnVcIrZv43EykUPZkExcYkGYJHmj/5xKTJ/HXHUwMDE1VVx0zjT43Vx1MDAwMY6mKkFcdTAwMDTSSi/WR+Sy3qysNSpcYlx1MDAwMGdcIlx1MDAxOPD051x1MDAxYtk+in7kQDaT6Fx1MDAwZqCrR4R+z8NcdTAwMTMkp7lcdTAwMTLKSTZjL5CbRuDHx+59byVcdTAwMWJo3bZarnc3gIRcdTAwMTT2SdEgP0mRXHK3SzM6PXDguuc2XHUwMDEzTlx1MDAxND27MUiW2K1ZXr87XHUwMDBlckNag1x1MDAxYltwubAyXCJcdTAwMTdB6DZd3/Kq/SDmoqiavI+iXHLVIIM4O+LZQt9US7aiXHUwMDFjXHUwMDA1XHUwMDFkQorgXHUwMDExknKMXHUwMDExJKl60Gz/bJJmsUwjqUnqO1xc5EzVUkg6PXVcdTAwMWLA1zwknTd1mIukd6tA0rvpJJ26fcTYRNMtlZGJY8mW2+eYKr/qzeq5+ra5xaPTXHUwMDFhoSdl91x1MDAxMs/H1OVtIFx0IdBwRs45RYxyOuDE5yps1pVNOOejXHUwMDFjZYlcdTAwMTCMddnSoLEuW1HCtFFkuVuZWsFgyFx1MDAxN1x1MDAxMGoqXHUwMDE2J+Z+Uk0sXHUwMDBlJU5CQs7D5cxA1MI76HSFdqtB6aLEq3LvXHUwMDFiu1xc9SVcdTAwMDOEXHUwMDE4XHQ96uu4ZFxic2rkK8G4iPpQsrPKwOjllvNlZH5cdTAwMWFzpfBcdTAwMGJA+fMyv/tNZ9uznS1TvdhVpVp5o0qP4tzq9atA9PhZQIFIYjXc2lx1MDAxN1x0WMhcYoXRnl0kqvbpl8P7vc+4XHUwMDFhhpVyffdr4/42Wn2RMIhcdTAwMWI6Ulwi4onfZJpqsshXXHUwMDFj5ilcdTAwMGVRXGYzwzWZx2a+TYnoVlx1MDAwZU7Lbjlyb732l42Se3h3fXw1oTiEXHUwMDA12Fx1MDAwM5gzpmBhXHUwMDE3ODd7hV/VodzDzpx6MjptN5Qkb4O9IPecPpUrqlx1MDAxMZKDg1SDfqHnalx1MDAxNdJcdTAwMGKWiNnqQzrZc9RCLcDK9mE0JvOcrvhcdTAwMDPwennmXHSCkzexv8pDUzia26JcdTAwMWbmqOGgXHUwMDExlJDZs87pjmxFOSpcdTAwMTRF4JuH8k7BKFqF0pDSsCwl3nS5/JyetlxyQGvl+flcdTAwMTYqQ5P4SfFEfjLNXHUwMDA1y7+g8lx1MDAxYztcdTAwMWT7vGxFYSm4a1xcXHUwMDFjXHUwMDFkXHUwMDE4vGNcImfl92HpSFxyJt1gwVx1MDAwNqnXloSeMdizsFx1MDAxM1wiXHUwMDEz4PfZkl++1FxmgzYtjUC/4Vx1MDAwZlx1MDAwNfz7KrDoMZK5qKRy7zRcctdXXHUwMDE5JpqJXHUwMDE3uNHKmXW4WflyXHUwMDE4XHUwMDFknYlcbrlukMvd49aqc4lrgyC/XHUwMDE53JZM7SjTi99cbpmRUZxcdTAwMWLMtDHLffFOU54vr/9iVGFcdTAwMTbzOHFt4jR5I1x1MDAwNM9eXHUwMDAxKlx1MDAxMa5cdTAwMGVD3N611i/3jsXJie9/2171/VxuqSmSckx6J4hChL9293/KloVcdTAwMWPzTutcdTAwMTguKYqTfzpcdTAwMTJcdTAwMGLYVpzGJVx1MDAwM7dd3upcdTAwMDTz3rTjVeDSYyQ9Lr17tMBFq90+jmGEik9lKphcdTAwMDS3/viY2fWKN67d3Vx1MDAxOIeC9JNcXDXlZ8JcdTAwMDU7mYJcdTAwMWZcdTAwMGbvXHUwMDFl/lx1MDAwNeEmVVx1MDAxOCJ9 XyXy(0, 0)(0, 0)Widget"},{"location":"guide/input/#mouse-movements","title":"Mouse movements","text":"

    When you move the mouse cursor over a widget it will receive MouseMove events which contain the coordinate of the mouse and information about what modifier keys (Ctrl, Shift etc) are held down.

    The following example shows mouse movements being used to attach a widget to the mouse cursor.

    mouse01.pymouse01.tcss mouse01.py
    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog, Static\n\n\nclass Ball(Static):\n    pass\n\n\nclass MouseApp(App):\n    CSS_PATH = \"mouse01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield RichLog()\n        yield Ball(\"Textual\")\n\n    def on_mouse_move(self, event: events.MouseMove) -> None:\n        self.screen.query_one(RichLog).write(event)\n        self.query_one(Ball).offset = event.screen_offset - (8, 2)\n\n\nif __name__ == \"__main__\":\n    app = MouseApp()\n    app.run()\n
    mouse01.tcss
    Screen {\n    layers: log ball;\n}\n\nRichLog {\n    layer: log;\n}\n\nBall {\n    layer: ball;\n    width: auto;\n    height: 1;\n    background: $secondary;\n    border: tall $secondary;\n    color: $background;\n    box-sizing: content-box;\n    text-style: bold;\n    padding: 0 4;\n}\n

    If you run mouse01.py you should find that it logs the mouse move event, and keeps a widget pinned directly under the cursor.

    The on_mouse_move handler sets the offset style of the ball (a rectangular one) to match the mouse coordinates.

    "},{"location":"guide/input/#mouse-capture","title":"Mouse capture","text":"

    In the mouse01.py example there was a call to capture_mouse() in the mount handler. Textual will send mouse move events to the widget directly under the cursor. You can tell Textual to send all mouse events to a widget regardless of the position of the mouse cursor by calling capture_mouse.

    Call release_mouse to restore the default behavior.

    Warning

    If you capture the mouse, be aware you might get negative mouse coordinates if the cursor is to the left of the widget.

    Textual will send a MouseCapture event when the mouse is captured, and a MouseRelease event when it is released.

    "},{"location":"guide/input/#enter-and-leave-events","title":"Enter and Leave events","text":"

    Textual will send a Enter event to a widget when the mouse cursor first moves over it, and a Leave event when the cursor moves off a widget.

    "},{"location":"guide/input/#click-events","title":"Click events","text":"

    There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a MouseDown event, followed by MouseUp when the button is released. Textual then sends a final Click event.

    If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states.

    "},{"location":"guide/input/#scroll-events","title":"Scroll events","text":"

    Most mice have a scroll wheel which you can use to scroll the window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle MouseScrollDown and MouseScrollUp if you want build your own scrolling functionality.

    Information

    Terminal emulators will typically convert trackpad gestures in to scroll events.

    "},{"location":"guide/layout/","title":"Layout","text":"

    In Textual, the layout defines how widgets will be arranged (or laid out) inside a container. Textual supports a number of layouts which can be set either via a widget's styles object or via CSS. Layouts can be used for both high-level positioning of widgets on screen, and for positioning of nested widgets.

    "},{"location":"guide/layout/#vertical","title":"Vertical","text":"

    The vertical layout arranges child widgets vertically, from top to bottom.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2ZW2/aSFx1MDAxNIDf8ytcIvrauHO/VFqtXHUwMDAyTdrck9KkTVdVNLFcdTAwMDdwMLbXXHUwMDFlXHUwMDEyoOp/37FJMVx1MDAxOFx1MDAxY1FcdTAwMWFRtrt+MPjM7XjmO2fOXHUwMDE5f93a3q6ZYaxrr7dreuCqwPdcdTAwMTL1UHuZye91kvpRaItQ/pxG/cTNa3aMidPXr171VNLVJlx1MDAwZZSrnXs/7asgNX3Pj1x1MDAxYzfqvfKN7qV/ZvdT1dN/xFHPM4lTXGayoz3fRMl4LFx1MDAxZOieXHUwMDBlTWp7/8s+b29/ze9T2iXaNSpsXHUwMDA3Om+QXHUwMDE3XHUwMDE1XG5CLsrS0yjMlYVcdTAwMDJRxFx1MDAxMGZsUsNP39jxjPZsccvqrIuSTFS7rNdcdTAwMGbj+/6RSNBdfFF/84lcdTAwMGZIUlxm2/KDoGmGQa6Wm0RputNRxu1cdTAwMTQ1UpNEXf3R90wn06Akn7RNIztcdTAwMTNFqyTqtzuhTtOZNlGsXFzfXGYzXHUwMDE5XHUwMDAwXHUwMDEz6XgmXm9cdTAwMTeSQbZOQjqSUFxmXHUwMDExncjzloI6XHUwMDE4gVx1MDAxOflYl0ZcdTAwMTTYJbC6vFx1MDAwMPlVaHOr3G7bqlx1MDAxNHqTOiZRYVx1MDAxYavELlRR7+HxLYlkXHUwMDBl5kJcdTAwMDI2NUhH++2OsaVcdTAwMThcdEdcdTAwMTDMp8bX+fxD24ZcdTAwMGLBWVGSjVx1MDAxYVx1MDAxZng5XHUwMDBiX8pz11FJ/DhHtTR7mNI4U3ZvXG6konE/9tR4vSFjXGJJiVx1MDAwMVx1MDAxN6yYvMBcdTAwMGa7tjDsXHUwMDA3QSGL3G6BSC799nJcdTAwMTU2KapiU2AkhSRkeTRcdTAwMTk4XCJcdTAwMThGh+qm8/Gi07jY8y/gTVx1MDAwNZolvGahROuDUlx1MDAwModIXHUwMDA0XHUwMDA1L0NJXHUwMDFjgGd5eX4oiVNBJGJcdTAwMGVEXHUwMDEwyFx1MDAwNUxcIkgpwFx1MDAxOK5cdTAwMTFJXGZcdTAwMDCUjHD0XFxI6iDw43QxkKjSWVxujJl1XHUwMDE0kixccuS+PLl517x6f/k+uvp09Fx1MDAwZXXdYd1bXHUwMDA1yPV5SVxmoFx1MDAwMyBlZSdpWSmJV8DxRUtRu+HMo1xikYMgmfWBXHUwMDEzXHUwMDE4IXRKbvu7e+SIQYq5/Fx1MDAxN3vHp1BcdTAwMTSVvpFbs4VcdTAwMThRsDSKny/SgTm6eXu81/Aurlx1MDAxM91NSXC04ShcIupQXHUwMDA2KGFz3lFcIuu5XHUwMDEwnt0zV+LxXHUwMDE2XHUwMDAw+lxcPFwiwFx0XHUwMDAzkEn8e1x1MDAwMmnjxCogXHQj2DpqKZZcdTAwMDbyslx1MDAxZTY/XHUwMDFm3lx1MDAwNVx1MDAwZnz0rn/gN+7vXFx8sOFAUuhAYO9cdTAwMGLcI3IwXHUwMDEwVP4skFx1MDAxMN1cbsGeXHUwMDBiSMKJpIKJ3zZ8xE+kNvaShEO8PJLnXHUwMDFmklH3nDY8vvu3fHOTXHUwMDA0QI3OKpDsKLfTT/RcdTAwMDZAXHSBXHUwMDAz5YJcdTAwMTDSukeHlZBZfc+mXHUwMDBivCQhwsk8nlxcSCW1Sc04r1wiMrtcdTAwMDQr44lcdTAwMTBEmGNcdTAwMDLXiifN7Eg8XHUwMDE3nkZcdTAwMGbMQl9Z6SohQ4xhgsDyiVxybV2NTsO3g+udYDS63mVR86ZcdTAwMTFvOpg2S5jlkZKf4fDJVIaRef5cdTAwMTbEi1x1MDAxNkKbSeBcco9cdTAwMTeLdY1C0/RHOlx1MDAwZi1mpPuq51x1MDAwN8OZpclBtJrahW5rMz2VqbZjjk97ZmrvXHUwMDA2fjtDtVx1MDAxNujWLMPGd1UwKTbR1Ju7dnRlu0tcdTAwMGW88ltEid/2Q1x1MDAxNXyY1WR1705cdTAwMTCv9u6SXHUwMDAwIaRcXD5cdTAwMDKW51x1MDAxMFx1MDAxY1x1MDAwZY7htZa3rZM9fkz76d6mXHUwMDFiXHUwMDExXHUwMDA20mFcdTAwMDLPXHUwMDA2XHUwMDE2w9ztXHUwMDEzR1xiJH7y1OpcdTAwMDVcdTAwMDEuoJwtXGI5XGIlXHUwMDBlp7jihFx1MDAwMELqXGLISFaaXHUwMDBmXHUwMDAz56yNXCJJKON0vdFcdTAwMDdcdTAwMDXUboZriT4oJlV8YmRcdTAwMTM0xvnyeKrPO83hzd6nncuTw+O6XHUwMDFj+Gr/Q3Pj8bTBXHUwMDA3gVxczmVoWWhApSzFXHUwMDA2K1x1MDAwMeoy3aKLXHUwMDAxtUFxJaCEOyjXazzIPJ9cYoBcZm+I15utUWhDtWfjUyVJ9LD4XHUwMDFjqzpX43ZcdTAwMGKU8Fx1MDAwN+KP+5OUjc5cdTAwMGUur0x8XHUwMDE2JOfDs1x1MDAwN4YvVmNzfUerwsa/slx1MDAxY1x1MDAwMH8/yypHrWUybVx1MDAxYaawfprMqlxcXHI4nHO8OFXDXGI5XHUwMDA0US5cdTAwMTZcdTAwMWZnXHRcblxiWyFcdTAwMWPONVt3eJJcdTAwMWGVmLpcdTAwMWZ6ftguN9GhV1FcdTAwMTKo1DSiXs83Vo3zyFx1MDAwZk25Rt7vblx1MDAwNnZHq7kow/Y8XVa2gDjrsfhSll3Fv+1cdTAwMDKR/GHy/8vLxbXnVjK7ptew6GFr+vdHs1x1MDAwNVwiy8JJoGMxlVhQtLy1plx1MDAwN/qwvdc+xfuXcrA/knetNiSbvpNYN+0ghNjc0Vxutnnk/LeIX5M/XHUwMDAwTG3ehjb9POW/lEBUWZR84ps3QcKGrj/wzftYJo2O2b3bXHUwMDFk6uOr3TPVc+/ev918i+JcdTAwMGVcdTAwMTFg/vScWIviXGbymVx1MDAxM6NfZFFYXHUwMDEwXHS53aP/t6i1W9TW475XU3HcNHaGbI2xfdlF8L3H1yz6q937+qG+6IQwv7JecyvN7EFnS/D129a3f1x1MDAwMLFE1Vx1MDAwMCJ9 WidgetWidgetWidget

    The example below demonstrates how children are arranged inside a container with the vertical layout.

    Outputvertical_layout.pyvertical_layout.tcss

    VerticalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass VerticalLayoutExample(App):\n    CSS_PATH = \"vertical_layout.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = VerticalLayoutExample()\n    app.run()\n
    Screen {\n    layout: vertical;\n}\n\n.box {\n    height: 1fr;\n    border: solid green;\n}\n

    Notice that the first widget yielded from the compose method appears at the top of the display, the second widget appears below it, and so on. Inside vertical_layout.tcss, we've assigned layout: vertical to Screen. Screen is the parent container of the widgets yielded from the App.compose method, and can be thought of as the terminal window itself.

    Note

    The layout: vertical CSS isn't strictly necessary in this case, since Screens use a vertical layout by default.

    We've assigned each child .box a height of 1fr, which ensures they're each allocated an equal portion of the available height.

    You might also have noticed that the child widgets are the same width as the screen, despite nothing in our CSS file suggesting this. This is because widgets expand to the width of their parent container (in this case, the Screen).

    Just like other styles, layout can be adjusted at runtime by modifying the styles of a Widget instance:

    widget.styles.layout = \"vertical\"\n

    Using fr units guarantees that the children fill the available height of the parent. However, if the total height of the children exceeds the available space, then Textual will automatically add a scrollbar to the parent Screen.

    Note

    A scrollbar is added automatically because Screen contains the declaration overflow-y: auto;.

    For example, if we swap out height: 1fr; for height: 10; in the example above, the child widgets become a fixed height of 10, and a scrollbar appears (assuming our terminal window is sufficiently small):

    VerticalLayoutScrolledExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2582\u2582 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502

    With the parent container in focus, we can use our mouse wheel, trackpad, or keyboard to scroll it.

    "},{"location":"guide/layout/#horizontal","title":"Horizontal","text":"

    The horizontal layout arranges child widgets horizontally, from left to right.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aa0/bSFx1MDAxNIa/8ytQ+rVM536ptFpcdTAwMTEuLTQtlNAtdFVVjj1JZnFsYztcdGnFf9+xQ+PE2GxcYlHKatdCSTzX45nnXHUwMDFjvzPDj63t7UY6iXTj9XZD37iOb7zYXHUwMDE5N15m6SNcdTAwMWQnJlxmbFx1MDAxNs7vk3BcdTAwMTi7ecl+mkbJ61evXHUwMDA2Tnyl08h3XFxcckYmXHUwMDE5On6SXHUwMDBlPVx1MDAxM1x1MDAwMjdcdTAwMWO8MqlcdTAwMWUkv2efXHUwMDFmnIH+LVxuXHUwMDA3Xlx1MDAxYYOik1x1MDAxZO2ZNIynfWlfXHUwMDBmdJAmtvU/7f329o/8c866WLupXHUwMDEz9HydV8izXG5cdTAwMDNcdTAwMTFH5dRcdTAwMGZhkFx1MDAxYitcdTAwMDRnTFBOZ1x1MDAwNUyyb7tLtWdzu9ZkXeRkSY1wrHY6o1x1MDAxMbuiTv8vfja8uWw3o6LXrvH9djrxc6vcOEySnb6Tuv2iRJLG4ZX+bLy0n9lWSp/VTUI7XHUwMDEwRa04XHUwMDFj9vqBTpKFOmHkuCadZGlcdTAwMTDOUqdcdTAwMDPxertIubF3XHUwMDFjXHUwMDAxXHUwMDA0XHUwMDEx45jNkvOKXHUwMDA0XHUwMDAyXHUwMDBlXHUwMDA1xUhcblYyZi/07Vx1MDAxNFhjXsD8KszpOO5Vz9pcdTAwMTR4szJp7Fx1MDAwNEnkxHaiinLju8ekilx1MDAwM1wipILz3fe16fVTm0uwXHUwMDA0kpL5/nU+XHUwMDAxXGJhXHUwMDAyle2ZzHKyXqMjL2fha3nw+k5cdTAwMWPdXHJSI8lu5izOjD2YXHUwMDAzqag8jDxnOuGIc4yVXCJcbjNajJ5vgiubXHUwMDE5XGZ9v0hcdTAwMGLdq4KRPPX25SpsXHUwMDEyXsemopxcdTAwMGKO4fJs9odB29//LryTZvzHXHUwMDA1jpLjT+ZdXHKbJb5cdTAwMTapxJukktPFuc8rYlx1MDAwNVx1MDAwNJSqRMXaqaSgXHUwMDA2ScxcdTAwMDHCXGKqKii55Vx1MDAxMVNcdTAwMDHR5qAkXHUwMDEwYiigQOuCUvu+iZJqJFx1MDAxMalDkiNCqP1cdTAwMTNLI/lpsPd2ctChqOd8eXt4zVvHcFx1MDAxZq2C5OZcdTAwMDKlwFx1MDAwMImFaDiNk1xuMIZcdTAwMDV5KpEvulx1MDAwZcNcZt+nXHUwMDExYYBRyVx1MDAxN2Y8XCJcdTAwMDQoI1xi36NcdTAwMTFbw1xiUYJvNERaKyGkXHUwMDFioZGLOlx1MDAxYe1gXHREkIBqaVx1MDAxY9E5vWmpTtg62lx1MDAxOUYnh63v3eB493njaN+cwlx1MDAwZYLi94mUgFx1MDAxMlXGYiVcIjtcdTAwMTCytVx1MDAxMVx0MWFIWiY3TyTeXHUwMDAwkVx1MDAxONfKSeuLUmJCKVmayPeuuL5pXlx1MDAxY6TiYHzcla0v+/1o/3lcdTAwMTOJsOWC2ZBTISaFXHUwMDE1ckzAJyOJcEdKvi4kXHUwMDExQlx1MDAxMCumXHUwMDE4+1x1MDAxNyP5oI7k9WtcdTAwMWOroZVgXG7hpZmM3r9PzpvDvdZxcn74RSr+5qzztobJvuP2h7H+9VRcblx1MDAwMaxywVx1MDAxMpWZXHUwMDE0XGYwWKZ19Vx1MDAxNzevopJZuShcdFOVWGIuXHUwMDAxrMKSWIVPIedqk1QqSlx1MDAxMCNiXVSm+iatVpGyXHUwMDE2SIVskGRKLS8jj1x1MDAwZttJ2mpCdlx1MDAxMnjJ8FrH74KLi+dO5DROksVcdTAwMTVGVlx1MDAxNUtcYlxixvjJSD64upnb1ChQrFjNWFxirYhcIlx1MDAxYlxczWShUVwiwsgjICzmOlxm0rb5rnOlsZB66FxmjD9ZmK6cTmupnfyeTufHMtG2z5xGuVB61ze9jN+Gr7uLYKfGdfxZdlx1MDAxYc49uWt7d2xz8ZFXfoowNj1cdTAwMTM4/vmiJatHeilonWNcdFx1MDAxYuelJIIv7Ve4NWiN+67eXHUwMDFm6dbHyWRMh2ejo+fuV5gxUN5cdTAwMWGYRnr7XG6AVns+OdJP1UdlpIdcdTAwMWPYt7pYeM3MRXpcdTAwMDZ4aZPtp59Jqz6gIHijXHUwMDEyRFFcdTAwMWJq4WP8bHUwXHUwMDE1YnVgXCJMoWBYLs3ldde9hEcn15+Cvve5N1x1MDAxYZs2Pfz23Lkkklx1MDAwMJS90O8pXHUwMDEwXHUwMDA0eFmarIIlxrKjq7HkXHUwMDE4SJ53QVV2iSo4XHUwMDE1kFx1MDAwNFcqXHUwMDExpCRWVJBccitcdTAwMTEsXHUwMDA15OuCs1aJqHouKYNcdTAwMWOJR+xnnYs3x6NT7/SjY051e6/3wfF26/aznlxymFhIIGBpVTZcdTAwMTVcIlx1MDAxMqh1bCGsQYhQqlxis4vHTetcdTAwMTAqXHUwMDFmJYb/Szqk1qPkXHUwMDAzXHUwMDEyhNs4w+dm8Vx1MDAxZlx1MDAwZtQ+XHUwMDFmOJff6NklvDa9sVaBOPlr8tw9imJcdTAwMDHKbvPToaDgpf3jX6TsieJ2sUfxpj0qO7T636Oyr3tcdTAwMWXlxHE4rnQpWOtSdrVoXHUwMDAzOH/EnuK3uHnK946uXHUwMDA35uDj8I1vlHmz667mUlx1MDAxYjxcdJSA4vu73FxmXCLwkCtJ0WWdp1x1MDAxY1x1MDAwMTJA+KK7XHUwMDE2+4mAK1U6XHUwMDE4v/MtaHOs4EUr7HHn1q3mW1xmMkVcdTAwMWVz6jJnh1x1MDAxM6dNXHUwMDEzeCbolavowKvJ8Z0k3Vx1MDAwYlx1MDAwN1x1MDAwM5NaM05DXHUwMDEzpOVcdTAwMTJ5u7tcdTAwMTnVfe3cc1x1MDAxMdvyfF5cdTAwMTn/KGux+K+O7Cp+bVx1MDAxN3zkN7PfX19Wlq6Yyewq5rBoYGv++3brrsmGXHUwMDEzRe3UXHUwMDBluDVo6rh2To13XHUwMDE3kYrnaoyMXHUwMDFlN6v2XHUwMDA38ytcdTAwMGJcdTAwMDC5+2d+prOn+3G7dfs38GbaXHUwMDA3In0= WidgetWidgetWidget

    The example below shows how we can arrange widgets horizontally, with minimal changes to the vertical layout example above.

    Outputhorizontal_layout.pyhorizontal_layout.tcss

    HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass HorizontalLayoutExample(App):\n    CSS_PATH = \"horizontal_layout.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalLayoutExample()\n    app.run()\n
    Screen {\n    layout: horizontal;\n}\n\n.box {\n    height: 100%;\n    width: 1fr;\n    border: solid green;\n}\n

    We've changed the layout to horizontal inside our CSS file. As a result, the widgets are now arranged from left to right instead of top to bottom.

    We also adjusted the height of the child .box widgets to 100%. As mentioned earlier, widgets expand to fill the width of their parent container. They do not, however, expand to fill the container's height. Thus, we need explicitly assign height: 100% to achieve this.

    A consequence of this \"horizontal growth\" behaviour is that if we remove the width restriction from the above example (by deleting width: 1fr;), each child widget will grow to fit the width of the screen, and only the first widget will be visible. The other two widgets in our layout are offscreen, to the right-hand side of the screen. In the case of horizontal layout, Textual will not automatically add a scrollbar.

    To enable horizontal scrolling, we can use the overflow-x: auto; declaration:

    Outputhorizontal_layout_overflow.pyhorizontal_layout_overflow.tcss

    HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass HorizontalLayoutExample(App):\n    CSS_PATH = \"horizontal_layout_overflow.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalLayoutExample()\n    app.run()\n
    Screen {\n    layout: horizontal;\n    overflow-x: auto;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    With overflow-x: auto;, Textual automatically adds a horizontal scrollbar since the width of the children exceeds the available horizontal space in the parent container.

    "},{"location":"guide/layout/#utility-containers","title":"Utility containers","text":"

    Textual comes with several \"container\" widgets. Among them, we have Vertical, Horizontal, and Grid which have the corresponding layout.

    The example below shows how we can combine these containers to create a simple 2x2 grid. Inside a single Horizontal container, we place two Vertical containers. In other words, we have a single row containing two columns.

    Outpututility_containers.pyutility_containers.tcss

    UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Vertical(\n                Static(\"One\"),\n                Static(\"Two\"),\n                classes=\"column\",\n            ),\n            Vertical(\n                Static(\"Three\"),\n                Static(\"Four\"),\n                classes=\"column\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n
    Static {\n    content-align: center middle;\n    background: crimson;\n    border: solid darkred;\n    height: 1fr;\n}\n\n.column {\n    width: 1fr;\n}\n

    You may be tempted to use many levels of nested utility containers in order to build advanced, grid-like layouts. However, Textual comes with a more powerful mechanism for achieving this known as grid layout, which we'll discuss below.

    "},{"location":"guide/layout/#composing-with-context-managers","title":"Composing with context managers","text":"

    In the previous section, we've shown how you add children to a container (such as Horizontal and Vertical) using positional arguments. It's fine to do it this way, but Textual offers a simplified syntax using context managers, which is generally easier to write and edit.

    When composing a widget, you can introduce a container using Python's with statement. Any widgets yielded within that block are added as a child of the container.

    Let's update the utility containers example to use the context manager approach.

    utility_containers_using_with.pyutility_containers.pyutility_containers.tcssOutput

    Note

    This code uses context managers to compose widgets.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            with Vertical(classes=\"column\"):\n                yield Static(\"One\")\n                yield Static(\"Two\")\n            with Vertical(classes=\"column\"):\n                yield Static(\"Three\")\n                yield Static(\"Four\")\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n

    Note

    This is the original code using positional arguments.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\n\n\nclass UtilityContainersExample(App):\n    CSS_PATH = \"utility_containers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Vertical(\n                Static(\"One\"),\n                Static(\"Two\"),\n                classes=\"column\",\n            ),\n            Vertical(\n                Static(\"Three\"),\n                Static(\"Four\"),\n                classes=\"column\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = UtilityContainersExample()\n    app.run()\n
    Static {\n    content-align: center middle;\n    background: crimson;\n    border: solid darkred;\n    height: 1fr;\n}\n\n.column {\n    width: 1fr;\n}\n

    UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    Note how the end result is the same, but the code with context managers is a little easier to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!

    "},{"location":"guide/layout/#grid","title":"Grid","text":"

    The grid layout arranges widgets within a grid. Widgets can span multiple rows and columns to create complex layouts. The diagram below hints at what can be achieved using layout: grid.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW3PaRlx1MDAxNMff8yk85DVsds/eM9PpONimwZc40NZ2O52MkFx1MDAxNiQjkCxcdOM4k+/elUhcdTAwMTFcYpNQyrhcdTAwMDQ9aEZnb8e7P875n/XnXHUwMDE3XHUwMDA3XHUwMDA3texTbGpvXHUwMDBlaubBdcLAS5xJ7VVuvzdJXHUwMDFhRCPbXHUwMDA0xXdcdTAwMWGNXHUwMDEzt+jpZ1mcvnn9eugkXHUwMDAzk8Wh41x1MDAxYXRcdTAwMWakYydMs7FcdTAwMTdEyI2Gr4PMXGbTn/P3hTM0P8XR0MtcdTAwMTJULlI3XpBFyXQtXHUwMDEzmqFcdTAwMTllqZ39T/t9cPC5eM95l1x1MDAxODdzRv3QXHUwMDE0XHUwMDAziqbSQVx1MDAwZbRqvYhGhbNSYYEpV3rWIUiP7HKZ8Wxrz7psypbcVLtpnVx1MDAxZl596NavLlx1MDAxZlx1MDAxZVx1MDAxYVx1MDAxZlx1MDAwNmN8d/rQKlftXHUwMDA1YdjJPoWFV25cdTAwMTKlad13Mtcve6RZXHUwMDEyXHLMVeBlvu1DKvbZ2DSyXHUwMDFiUY5KonHfXHUwMDFmmTRdXHUwMDE4XHUwMDEzxY5cdTAwMWJkn3JcdTAwMWLGM+t0I95cdTAwMWOUloeiXHUwMDA3Q1RSzISSfNZSjFx1MDAxNVxuXHUwMDExxjglwCvuNKLQXHUwMDFlgnXnJS6e0qGu41x1MDAwZfrWq5E365MlziiNncRcdTAwMWVV2W/y9Vx1MDAwZmVa2OWVxmJuXHUwMDEx31x1MDAwNH0/s61cdTAwMTRcdTAwMTRSjM45lpriXGKIXHUwMDEyUlGQsjzCfNX4nVfQ8Fd1+3wnib9uUy3NP+Y8zp09nkOpXHUwMDFjPI49Z3rkRFxioFhcYo0xLfcvXGZGXHUwMDAz2zhcdTAwMWGHYWmL3EFJSWH98mpcdTAwMDM6iYaVdFxuKblQhKxN51x1MDAxOVHtWH7sezeP/lHnulx1MDAwMVx1MDAwZbm+XUFnhbBFLuFZuVx1MDAwNFxuXGbIMpdcdTAwMTIxXCL1XHUwMDAysNvnkqFcdTAwMTVQgkBcdTAwMDRcYtZPYSmVXHUwMDEyhCtJfmAsTVx1MDAxOFx1MDAwNnH6NJRCroKSgKCSYGB8bSpvvZPTNlx1MDAxN73r5nEr6pCscXtydrZcdJXPXHUwMDE4LVx0Q0phxVx1MDAxN06/XHUwMDE4K1x1MDAwNdJcdTAwMTJcdTAwMDRcdTAwMTf/LVq+7DlcdTAwMWM4LFx1MDAxM0lcdTAwMDBcdTAwMDFhi9FwxiQhqFx1MDAxYainRCrGXGLYUfr5gSTPXHUwMDAwJFx1MDAwMFlccqSywYNpvj6QN2MxOUs8t3nphONcdTAwMTNcdTAwMTI770izu+NAUo1cdTAwMThwqebPfsojR1x1MDAxNUw3o7GLMd9cdTAwMTaNVHPFlFx1MDAwNrmnNM5tRpVGLG101DZGrk2j4dK9ad+178bvo7fHl0o49cbJjtMobNZcXFx1MDAxNpJcdTAwMTbFLVx1MDAwNEZcdTAwMDJdm123hVwiyVx1MDAxMzVVWvxcdTAwMGapemssfltBXHUwMDEyvlJCMlxuQtn6Zv1cdTAwMDKn58i02ei/u3J9OrlodY/Os4+rkrXvuP44MTvAI1x1MDAxMMujkMtMgqVmSV1unq7FU1xcaoxA0pzL6UR0XHUwMDE5T1x0/0hZpotnXHRTomxxROeF195hOldmV6OmxsDtr1x1MDAxNNbnNFx1MDAxOep2M2xcdTAwMWReXHUwMDBl07f3LGh15MVxvOucUmo5kFhiWk3jgDXKz2KxXHUwMDE22S6oXG4jOc8p2YRT4FQrofT+ckphdfGDqS3KQdP1Of2j0W1cdTAwMDa3V4T83nPdX25cdTAwMWb751x1MDAxN0fOrnNq01x1MDAwNlJSaL7MqY2nXFxhvChEt1x1MDAxY1CZLb6YzJXElEK5XGZqrkB4cZc1XVx1MDAwYosqqfZcdTAwMTclOFx1MDAwNjJcdTAwMTf3941UXHUwMDAyeCWpXHUwMDAyXHUwMDEzRYGT9XXo+aVw621y18x4NOanp93eIVx1MDAxOe06qXnmZ5zj5cKIYkBWjW8h9U9cdTAwMDXp06mfXCLNXGLLg/bmqZ8qZXOCwPurUC2H30j9Np3k97xrg0rYUVx1MDAwMHU2oYG+gUFrXHUwMDEyTn5cdTAwMTk+7DqolDKElV4sX6ac2lpKq8rF/HY53U7mXHUwMDE33Go0YPub+blcdTAwMTIrMZVC2OPjZP1bps71yfk5jDJcdTAwMGbjTP7620X9LIuPdlx1MDAxZNOikpJM4KWLT6ol4lpXWjbhXHUwMDE0QHXNk5yCJohcbrlcdTAwMTCx/1xyofm9PFx1MDAxMXR/XHUwMDAzKdPf0Ka5XHUwMDFlgLl7ju9cdTAwMDH6cVx1MDAxON++T4Pe44fw0L+Pzvw2ucG7XHUwMDBlKLNxlC39X2ZcdTAwMDboVqTpakBcdFx1MDAxM0hPr2FXStPvclxuWFx1MDAxMK2A/Vx1MDAxOJf19l1MWnPiuJPZKW3zlFrrdeB1gkezME3tPjCTt09cdP7iqb34yn7Ol8l9/vzlxZe/XHUwMDAxUO5ccsMifQ==

    Note

    Grid layouts in Textual have little in common with browser-based CSS Grid.

    To get started with grid layout, define the number of columns and rows in your grid with the grid-size CSS property and set layout: grid. Widgets are inserted into the \"cells\" of the grid from left-to-right and top-to-bottom order.

    The following example creates a 3 x 2 grid and adds six widgets to it

    Outputgrid_layout1.pygrid_layout1.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout1.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3 2;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    If we were to yield a seventh widget from our compose method, it would not be visible as the grid does not contain enough cells to accommodate it. We can tell Textual to add new rows on demand to fit the number of widgets, by omitting the number of rows from grid-size. The following example creates a grid with three columns, with rows created on demand:

    Outputgrid_layout2.pygrid_layout2.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Seven\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout2.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n        yield Static(\"Seven\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Since we specified that our grid has three columns (grid-size: 3), and we've yielded seven widgets in total, a third row has been created to accommodate the seventh widget.

    Now that we know how to define a simple uniform grid, let's look at how we can customize it to create more complex layouts.

    "},{"location":"guide/layout/#row-and-column-sizes","title":"Row and column sizes","text":"

    You can adjust the width of columns and the height of rows in your grid using the grid-columns and grid-rows properties. These properties can take multiple values, letting you specify dimensions on a column-by-column or row-by-row basis.

    Continuing on from our earlier 3x2 example grid, let's adjust the width of the columns using grid-columns. We'll make the first column take up half of the screen width, with the other two columns sharing the remaining space equally.

    Outputgrid_layout3_row_col_adjust.pygrid_layout3_row_col_adjust.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout3_row_col_adjust.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 2fr 1fr 1fr;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Since our grid-size is 3 (meaning it has three columns), our grid-columns declaration has three space-separated values. Each of these values sets the width of a column. The first value refers to the left-most column, the second value refers to the next column, and so on. In the example above, we've given the left-most column a width of 2fr and the other columns widths of 1fr. As a result, the first column is allocated twice the width of the other columns.

    Similarly, we can adjust the height of a row using grid-rows. In the following example, we use % units to adjust the first row of our grid to 25% height, and the second row to 75% height (while retaining the grid-columns change from above).

    Outputgrid_layout4_row_col_adjust.pygrid_layout4_row_col_adjust.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout4_row_col_adjust.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 2fr 1fr 1fr;\n    grid-rows: 25% 75%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    If you don't specify enough values in a grid-columns or grid-rows declaration, the values you have provided will be \"repeated\". For example, if your grid has four columns (i.e. grid-size: 4;), then grid-columns: 2 4; is equivalent to grid-columns: 2 4 2 4;. If it instead had three columns, then grid-columns: 2 4; would be equivalent to grid-columns: 2 4 2;.

    "},{"location":"guide/layout/#auto-rows-columns","title":"Auto rows / columns","text":"

    The grid-columns and grid-rows rules can both accept a value of \"auto\" in place of any of the dimensions, which tells Textual to calculate an optimal size based on the content.

    Let's modify the previous example to make the first column an auto column.

    Outputgrid_layout_auto.pygrid_layout_auto.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502First\u00a0column\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout_auto.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"First column\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-columns: auto 1fr 1fr;\n    grid-rows: 25% 75%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Notice how the first column is just wide enough to fit the content of each cell. The layout will adjust accordingly if you update the content for any widget in that column.

    "},{"location":"guide/layout/#cell-spans","title":"Cell spans","text":"

    Cells may span multiple rows or columns, to create more interesting grid arrangements.

    To make a single cell span multiple rows or columns in the grid, we need to be able to select it using CSS. To do this, we'll add an ID to the widget inside our compose method so we can set the row-span and column-span properties using CSS.

    Let's add an ID of #two to the second widget yielded from compose, and give it a column-span of 2 to make that widget span two columns. We'll also add a slight tint using tint: magenta 40%; to draw attention to it.

    Outputgrid_layout5_col_span.pygrid_layout5_col_span.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502\u2502Four\u2502\u2502Five\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Six\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout5_col_span.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two [b](column-span: 2)\", classes=\"box\", id=\"two\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n#two {\n    column-span: 2;\n    tint: magenta 40%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Notice that the widget expands to fill columns to the right of its original position. Since #two now spans two cells instead of one, all widgets that follow it are shifted along one cell in the grid to accommodate. As a result, the final widget wraps on to a new row at the bottom of the grid.

    Note

    In the example above, setting the column-span of #two to be 3 (instead of 2) would have the same effect, since there are only 2 columns available (including #two's original column).

    We can similarly adjust the row-span of a cell to have it span multiple rows. This can be used in conjunction with column-span, meaning one cell may span multiple rows and columns. The example below shows row-span in action. We again target widget #two in our CSS, and add a row-span: 2; declaration to it.

    Outputgrid_layout6_row_span.pygrid_layout6_row_span.tcss

    GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02\u00a0and\u00a0row-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502\u2502 \u2502Three\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout6_row_span.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two [b](column-span: 2 and row-span: 2)\", classes=\"box\", id=\"two\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\napp = GridLayoutExample()\nif __name__ == \"__main__\":\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n}\n\n#two {\n    column-span: 2;\n    row-span: 2;\n    tint: magenta 40%;\n}\n\n.box {\n    height: 100%;\n    border: solid green;\n}\n

    Widget #two now spans two columns and two rows, covering a total of four cells. Notice how the other cells are moved to accommodate this change. The widget that previously occupied a single cell now occupies four cells, thus displacing three cells to a new row.

    "},{"location":"guide/layout/#gutter","title":"Gutter","text":"

    The spacing between cells in the grid can be adjusted using the grid-gutter CSS property. By default, cells have no gutter, meaning their edges touch each other. Gutter is applied across every cell in the grid, so grid-gutter must be used on a widget with layout: grid (not on a child/cell widget).

    To illustrate gutter let's set our Screen background color to lightgreen, and the background color of the widgets we yield to darkmagenta. Now if we add grid-gutter: 1; to our grid, one cell of spacing appears between the cells and reveals the light green background of the Screen.

    Outputgrid_layout7_gutter.pygrid_layout7_gutter.tcss

    GridLayoutExample OneTwoThree FourFiveSix

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass GridLayoutExample(App):\n    CSS_PATH = \"grid_layout7_gutter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"One\", classes=\"box\")\n        yield Static(\"Two\", classes=\"box\")\n        yield Static(\"Three\", classes=\"box\")\n        yield Static(\"Four\", classes=\"box\")\n        yield Static(\"Five\", classes=\"box\")\n        yield Static(\"Six\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = GridLayoutExample()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3;\n    grid-gutter: 1;\n    background: lightgreen;\n}\n\n.box {\n    background: darkmagenta;\n    height: 100%;\n}\n

    Notice that gutter only applies between the cells in a grid, pushing them away from each other. It doesn't add any spacing between cells and the edges of the parent container.

    Tip

    You can also supply two values to the grid-gutter property to set vertical and horizontal gutters respectively. Since terminal cells are typically two times taller than they are wide, it's common to set the horizontal gutter equal to double the vertical gutter (e.g. grid-gutter: 1 2;) in order to achieve visually consistent spacing around grid cells.

    "},{"location":"guide/layout/#docking","title":"Docking","text":"

    Widgets may be docked. Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container. Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2aWXPbNlx1MDAxMIDf8ys0ymvM4D4y0+nItpKocWwnjuOj0+nQJCTSokiWpCxLXHUwMDE5//cuaVXURVx1MDAxZrKqqtGDLWFcdGBcdHy72Fx1MDAwNfDjVa1Wz4axqb+r1c2tY1x1MDAwN76b2IP6m7z8xiSpXHUwMDFmhSBcIsXvNOonTvGkl2Vx+u7t256ddE1cdTAwMTZcdTAwMDe2Y6xcdTAwMWI/7dtBmvVdP7KcqPfWz0wv/TX/e2j3zC9x1HOzxCo72TGun0XJfV8mMD1cdTAwMTNmKbT+O/yu1X5cdTAwMTR/p7RLjJPZYScwRYVCVCrIXHSbLz2MwkJZzDXHTCuMJk/46T70l1x1MDAxOVx1MDAxN8Rt0NmUkryovk9cdTAwMWKNLDiPm7vvveD4YtQ6b3mdstu2XHUwMDFmXHUwMDA0J9kwKNRykihNdzw7c7zyiTRLoq45893MyzWYK5/UTSNcdTAwMTiJslZcdTAwMTL1O15o0nSmTlx1MDAxNNuOn1xy8zJUvsL9SLyrlSW3+TBcYksppLimkk9cdTAwMDR5VVwiLc0wQ4LwOWX2olx1MDAwMOZcdTAwMDCUeY2KT6nOle10O6BT6E6eyVx1MDAxMjtMYzuBmSqfXHUwMDFijF+TaWFRqfRMJ57xO15cdTAwMDZSSpSl2LReqSkmQFLMXHUwMDE4RVxcTVx1MDAwNHmnccstWPhjfuw8O4nHY1RP81x1MDAxZlNcbue6NqdAKiv3Y9e+n28sXHUwMDA0YVx1MDAxYyOtJKJcdTAwMTN54IddXHUwMDEwhv0gKMtcIqdbXCJSlN69WYFNrGklm4wjXCI1k09nM+yfXHUwMDFj73qX3/rdXHUwMDEz3jvY/Vx1MDAxNCFf7lWwOcfXLJVkk1RKhlxiZUuoJJjNUbF2KplVgSRcdTAwMTFcdTAwMTYmQMIyKJHGXHUwMDFhXHUwMDFjXHUwMDA3+lx1MDAxZkNpgsCP0+VIXG5VhaQg4D84werJRLZUvG9uj6L9a9p2/mzEPlx1MDAxYuhwXHUwMDE1XCI35yeFtKhSQmI1RyRcdTAwMDVUheZcdTAwMTS/zE++btuccLJIIyaLxE94xNhic13f08hcdTAwMDVSQlx1MDAwYkV+Tlx1MDAxYVx0IVU0YiykJuA7no6jz0dnXel+uP14ddL63CW94Pw82G5cdTAwMWM1tjSli6s25ZbgL1xcslx1MDAwMcUrhPi6UCyCXGLNlPhJUZSiXG5FmFx1MDAxZKQwsPhkXHUwMDEym8R8O/R6XzzxvXf05f1+s5mNXHUwMDBltptEjKXF5Cx0Y1x1MDAxMl9cdTAwMWE7vsbkXG587rpA1FgqJqT8P6/QXHUwMDBmh41cdTAwMTC5VLpFSlx1MDAxOVx1MDAxM1x1MDAxMOc/PW68uTj8XHUwMDFj68ug2T/5yptcdTAwMTk7v4pdv1x1MDAwMkbPdrx+Yv57XHUwMDFjuVx1MDAwNjTIXFxKkVfldJ3rtFhGJdWWXHUwMDEwRcZ031x1MDAxMF2Ek1wiYtFZ5cZ0XG4miCZywys2jFx1MDAwNkViM3TKXHUwMDA3Mm4qXGJoJJ+R1Vx1MDAxY1x1MDAxZYlcdTAwMTZpXFz3XHUwMDAzX35cdTAwMTmq9vfWzaCRbjudXHUwMDA0c0stYJjXXHUwMDE11FKQ2NGXZjZjn7mMT8iYLYZcdTAwMTHmS1NcdTAwMWJJQCghZJRMXHUwMDE3n3lAMVx1MDAwNF2SK0o2SygkXHUwMDE3SMrNXHUwMDEwqpSsXCKU5IqoZ1x1MDAxMbpz9MGcfo/OzcDP1M7p8V+O23K2n1BlgUdcdTAwMTBswYFiziyBJCMzKdB6XHUwMDExJVpZfFVAXHUwMDA1olQwyjaLJ5jFxvBElVmPRlhTNZ1cdTAwMTY9Rif969PnVn/onO3tfrn0mmfNTvL5z+2nXHUwMDEzsu2lyzthzFKU6lx1MDAxN29ccj1Cp0RcdTAwMTDN4dVcdTAwMWMoQ+A/MaGbXHJAYc2hXHUwMDAyb4RQgmTlLlx1MDAxMURHVEJcdTAwMWH4jGyo8enyMOy6UUz6R3vu7c23bD+Mt1x1MDAxZFFOmSU5X7KjLoWl+Dr8JyHqyiwlXHUwMDE0wotcIrZcdTAwMThcdTAwMTOotVjkXHUwMDE0XHUwMDBibTGxdFx1MDAwZlx1MDAxM1x1MDAxMiRcblx1MDAxM1x1MDAwNPRveIlcdTAwMTeYboZQrFHl1rpSmmBKpnKox1x1MDAwMFx1MDAxNa33e8e/XHRcdTAwMTnstM2+STJFXHUwMDBm+61tXHUwMDA3NF/hXHRcdTAwMTWUqPlcdTAwMTiUQs4uOV5DkvSQXHUwMDBmXHUwMDA1K1x1MDAxOJ83XHUwMDE1XHItI1RbsJapXHUwMDEyYzpcdTAwMGYqpVx1MDAxNFDlerMnQDBykESvi1M7SaLBUi/KKlx1MDAxMWWUS4LwM1x1MDAwZSbbl1xyXHUwMDE571x1MDAwN5/E6FwiuKTy4243XHUwMDFjXHUwMDBlVkN0c8c/WECaRDBmRFx1MDAxMIWEwnKGU5afTT5cZil3NEPuqpDmm1xiXHUwMDEwXHUwMDAyc5jyonu2yChB2lr0n1x1MDAwMoPqXG6vcjJZKLdcdTAwMWGXlMKQPsd/TulhJ9muXHUwMDFmun7YXHUwMDAx4T+M1ian661cdTAwMDKig49fvUBcdTAwMGVcdGmedk4v5ECnX9HpRNdcdTAwMWOjyOnnWu4gXHUwMDBiwlxywlx1MDAxNbhcdTAwMTdCNSyDik891rHjYv4teZ/ojiV3XHUwMDEzfUzoltrMvoCdZntRr+dn8OrHkVx1MDAxZmbzT1x1MDAxNO/SyI3KM7Y7L4WWp2Xz1lx1MDAxN+ctlldcdPJP+a1W4ln8mHz/483Sp3cq+SmkXHUwMDA1OmVcdTAwMWKvpv9XOYvM3GZLfVx1MDAwNa5cZrgkg0BcdTAwMTRcdTAwMDLO0sE+5itcdTAwMWWe5i1dznCxnC1cdTAwMWNcdTAwMTQzXG6xOnn5WUi1k4BwfolbWPBcdFx1MDAxME9cdIhcdP+N449cdEM/pvB6kt+fYeveXHUwMDE0JpK7f4D8N1x1MDAxY047XG6zXHUwMDEzf1TsqKCZ0vd2z1x1MDAwZoYzXHUwMDE4XHUwMDE00OeXa4q2ajDwXHUwMDFkk01PV2qg61wiuVAzlVx1MDAxYYHfya2jXHUwMDFlmPas2WS+Y1x1MDAwN1x1MDAxM3FcdTAwMTZNXHKvXHUwMDAzStjQXFzSWnBcdTAwMWVR4nf80Fx1MDAwZb4tVWglw8XVW02QIVxuSVx1MDAxOFJPz5RcdTAwMGVcdTAwMDJzdINij35cdTAwMThdOFe/XVx1MDAwZkbBaMWt+s2t8lRLaz6NXHUwMDA3O7aUnj+/WfNcdTAwMDVcdTAwMGbMS4VcdTAwMWawXFyMXHUwMDE01lx1MDAxMHuwXHKb7uHxdYNeKXamlZdcXCeDXHUwMDBmo1x1MDAwMTtYm+lyjMRz9qteZrpcdTAwMDf2MOpnY0tJt8F25zRaMUTn1Vx1MDAxN7RcdTAwMTCh+DnXs1x1MDAxZZ7uLbVdxoVFXHUwMDE5yq/hIaXx1GWL+1x1MDAwMJ1Z+rG7g0q2+dXqRqykhbjGeqzA1MZTadJcdTAwMTBcdTAwMWFUXdeCcsGJ5qusyy9cdNVz81OrmN9TQ/VcdTAwMDdXgtlQXHUwMDFkVpj8SFxcUYihJFXlJJahOrG0QkyonzdWr+SokE4jVFx1MDAxNbK/XHUwMDFhN1634/gkg/meTFx1MDAwZiDlu2OfWb5h/cY3g91lR8vFJ3dJxSjnpm/y9/xx9+rub5B4Q/4ifQ== Docked widgetLayout widgets

    To dock a widget to an edge, add a dock: <EDGE>; declaration to it, where <EDGE> is one of top, right, bottom, or left. For example, a sidebar similar to that shown in the diagram above can be achieved using dock: left;. The code below shows a simple sidebar implementation.

    Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

    DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout1_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above).

    Docking multiple widgets to the same edge will result in overlap. The first widget yielded from compose will appear below widgets yielded after it. Let's dock a second sidebar, #another-sidebar, to the left of the screen. This new sidebar is double the width of the one previous one, and has a deeppink background.

    Outputdock_layout2_sidebar.pydock_layout2_sidebar.tcss

    DockLayoutExample Sidebar1Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. \u2587\u2587 Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout2_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar2\", id=\"another-sidebar\")\n        yield Static(\"Sidebar1\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\napp = DockLayoutExample()\nif __name__ == \"__main__\":\n    app.run()\n
    #another-sidebar {\n    dock: left;\n    width: 30;\n    height: 100%;\n    background: deeppink;\n}\n\n#sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    Notice that the original sidebar (#sidebar) appears on top of the newly docked widget. This is because #sidebar was yielded after #another-sidebar inside the compose method.

    Of course, we can also dock widgets to multiple edges within the same container. The built-in Header widget contains some internal CSS which docks it to the top. We can yield it inside compose, and without any additional CSS, we get a header fixed to the top of the screen.

    Outputdock_layout3_sidebar_header.pydock_layout3_sidebar_header.tcss

    DockLayoutExample Sidebar1DockLayoutExample Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout3_sidebar_header.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"header\")\n        yield Static(\"Sidebar1\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n

    If we wished for the sidebar to appear below the header, it'd simply be a case of yielding the sidebar before we yield the header.

    "},{"location":"guide/layout/#layers","title":"Layers","text":"

    Textual has a concept of layers which gives you finely grained control over the order widgets are placed.

    When drawing widgets, Textual will first draw on lower layers, working its way up to higher layers. As such, widgets on higher layers will be drawn on top of those on lower layers.

    Layer names are defined with a layers style on a container (parent) widget. Descendants of this widget can then be assigned to one of these layers using a layer style.

    The layers style takes a space-separated list of layer names. The leftmost name is the lowest layer, and the rightmost is the highest layer. Therefore, if you assign a descendant to the rightmost layer name, it'll be drawn on the top layer and will be visible above all other descendants.

    An example layers declaration looks like: layers: one two three;. To add a widget to the topmost layer in this case, you'd add a declaration of layer: three; to it.

    In the example below, #box1 is yielded before #box2. Given our earlier discussion on yield order, you'd expect #box2 to appear on top. However, in this case, both #box1 and #box2 are assigned to layers which define the reverse order, so #box1 is on top of #box2

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"guide/layout/#offsets","title":"Offsets","text":"

    Widgets have a relative offset which is added to the widget's location, after its location has been determined via its parent's layout. This means that if a widget hasn't had its offset modified using CSS or Python code, it will have an offset of (0, 0).

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZWVPbSFx1MDAxMIDf+Vx1MDAxNZTzXHUwMDFhK3NcdTAwMWap2triXlxiS8JcdTAwMTVcYlspSkhjWWtZUqQx2Enx37clXHUwMDFjS744XGZhXHR6sK3p0Uxr5utr/GNpeblhXHUwMDA3qWm8X26YvudGoZ+5V423RfulyfIwiUFEyvs86WVe2bNtbZq/f/eu62ZcdTAwMWRj08j1jHNcdTAwMTnmPTfKbc9cdTAwMGZcdTAwMTPHS7rvQmu6+Z/F557bNX+kSde3mVNN0jR+aJPsZi5cdTAwMTOZroltXHUwMDBlo/9cdTAwMDP3y8s/ys+adpnxrFx1MDAxYlx1MDAwN5EpXHUwMDFmKEWVgpywyda9JC6VJVxcaM1cdTAwMDRBo1x1MDAwZWG+XHUwMDBl01njg7RcdTAwMDUqm0pSNDU2/L/pRvvbv70g+1x1MDAxNlxmVulgTZ82q1lbYVx1MDAxNFx1MDAxZNpBVGrlZUmeN9uu9dpVj9xmScechL5tQ1x1MDAxZjzRPno2T2AhqqeypFx1MDAxN7Rjk+djzySp64V2ULSh6lx1MDAxNW5cdTAwMTbi/XLV0i96UO4ohVx1MDAxNNdU8pGkeJYw7miGXHUwMDE5XHUwMDEyhE+os5ZEsFx0oM5cdTAwMWJUXpVCXHUwMDE3rtdcdECr2Fx1MDAxZvWxmVx1MDAxYuepm8FWVf2uhi/KtHCoVHpskrZcdIO2XHUwMDA1KSXKUayuWG7KLSCIYKWkpnokKWZNt/2Shq+Ty9d2s3S4TI28uKlpXFwou1FDqXq4l/ruzZZjIWA5XGJcdTAwMTDBdbV+UVx1MDAxOHdAXHUwMDE496Koaku8TkVJ2Xr9dlx1MDAwMTqxpvPoxJxgyTGW5N54XHUwMDFl2Y39081wV7ubXHUwMDFiJjv1z9OrvXl4TiA2XHUwMDBlJnlWMCVDhLJZYFx1MDAxMswmwHhyMJkzh0pcIlx1MDAxY0ww0jO4xEpQyonC9Dfm0kRRmOazqVx1MDAxNGoulVJcdTAwMTJcdTAwMDFb8lx1MDAwMCqT1Ytm/7CXx53Vg72TY4hcdTAwMDWfcGdcdTAwMTEqn9FdMvBXSlx0idUklZw5UmhO8ePc5ZuWy1x0J9NEYjJN/YhJjFx1MDAxZDYx9VxykUIrXHUwMDBlNqTx61x1MDAwNJKQuW5SIcxcdTAwMDVcdTAwMTX8/jz2XHUwMDBljoONwzPvUH6x4V/euWKnweZcdTAwMGLnUVwiR4PLmY7enDqTcXUxXHUwMDFhL1x1MDAxMOJPRaNUQkjxat0jkWKue0RCYK5FrcedOaV111vrQaY656q/x21rv1x1MDAxYixcdTAwMTS0n1x1MDAxMUdI55hcdTAwMTS8Tt1PXHUwMDFhXHUwMDFmjVwiJlx1MDAxN+B5n1xuRXDgiEtBlfyNWbw9hYQ0cS6OTCHN1Vx1MDAwM2hcXNvaOuqibbZ1XHUwMDE2fVx1MDAxOFxmdnbyj0r259DYdr12LzP/P49Eclx1MDAwN3EhxouYkkilXHUwMDFjOonq4uFazOJSI4fIMn+9XHUwMDE5iE7jKYnDsNRCSabLa1xuU1akXHUwMDE0XHUwMDE0iV9Q6VxmXHUwMDA1XHUwMDE1V7Xt3jnZXHUwMDBm6cpcdTAwMGU/kGebO1v+Jv34feXbaKwxXGLdLEuuXHUwMDFhI8n18NdcdTAwMGIxXHUwMDAyRvlcXCNcdTAwMTBcdTAwMTLyOMJqSe1dVtBstdbTXcp2dz+fb1xyzk74v51IvXQroFx1MDAxYUNcdTAwMTlccu+JucRIsYlUgVx1MDAxMeJcYqxcdTAwMWZf6Vx1MDAwZj30bFugo8OEhW2BgsdiYFxmr8pcdTAwMTRcbv+An8dcdTAwMTSEnpueXHUwMDEwpLDWhUO8tylE6+GXo1x1MDAwM2mClt7vtN0zkXhb+Us3hVwiICguXHUwMDE5marfXHUwMDE4l1x1MDAwZSNS0sdcdTAwMWUr3Fx1MDAxNlx1MDAxMVx1MDAxNHNcdTAwMTRReGhcdTAwMDNcYolcdTAwMDWsXHUwMDAwXHUwMDA24Fx1MDAwNKL373z0dWMnM931/FxmmjHQXHUwMDAxq/s769tccvdBhD7fuVx1MDAxN9RcdTAwMTDFiShDgKnCUvFxTGmR0eC7jlx1MDAxOZRs8YvFXHUwMDBmv6BYccCPa4Uxhe3neFx1MDAxYdPicJgzpYnSklx1MDAwYkbVJKZQknIsXHUwMDE5W6DUKzVdXHUwMDEw08J8yVx1MDAwMzCt6eFmdjWM/TBcdTAwMGVAWMWBn/8zbN8jXHIuqEq8Xrn7XHUwMDBllpD4gVx1MDAxOeNcItxcdTAwMDG8tU6Bm5ZEO1BcdTAwMTiVNfpQdD1Sx8T+3crcno3UlGlcIlx1MDAwN4FLwVx1MDAwNPJdpTVcdTAwMDRRxKbUUY7kRCvGOIJccuVCTilcdTAwMTW5uV1Lut3QwqJ/SsLYTi5uuYorhXW3jetPSuGl6rJJN5BcdTAwMTYjjofj6tdyZSflzej317cze89luLim6K1GW6p/z/Nf1vTtLPfF0S3uiyNYf0iB7u2/Lq8+dE6PV7+vN9m2/Cy7l3ZggpdcdTAwMWVhsdLgv1x1MDAwNNNMICaAqGpFSv8llFx1MDAwM9ZQxC+tMUTbx4TaW51YLauvju6nj1x1MDAwMSTkpFJcdTAwMTDyzMdcdTAwMDBcdTAwMTTXI9lcdTAwMDP8VCuJ7WH4/SZpXHUwMDFia910u2E0XHUwMDE427iSU9D0Y6uVXHUwMDFiW1/L3MCcJZdqrPdKXHUwMDE0XHUwMDA2cZnemdY44jb03GgktkntzT2Y3YXhsu0pk0+yMFxiYzc6XHUwMDFh1+RcdTAwMTFZLNdkro1xjFx1MDAwNUVS3d/G9pvHXHUwMDFl2ewrlVE//JCGe8dcdTAwMWLu2Vx1MDAxM9uYn1x1MDAxNP7yaZNcdTAwMDTmcKRnnLRRSVx1MDAxZHDv4tf+bfsk5Vx1MDAxYyZcZnKEwlx1MDAxYV5TPVfkyVx1MDAwZq/nloaDNtw0PbQw5Cjqw5qE/tDgq2FcdTAwMWGXoblanVV8lFehcmldXHUwMDA1wKZYkVx1MDAxZtdL1/9cdTAwMDE0elVbIn0= Offset

    The offset of a widget can be set using the offset CSS property. offset takes two values.

    • The first value defines the x (horizontal) offset. Positive values will shift the widget to the right. Negative values will shift the widget to the left.
    • The second value defines the y (vertical) offset. Positive values will shift the widget down. Negative values will shift the widget up.
    "},{"location":"guide/layout/#putting-it-all-together","title":"Putting it all together","text":"

    The sections above show how the various layouts in Textual can be used to position widgets on screen. In a real application, you'll make use of several layouts.

    The example below shows how an advanced layout can be built by combining the various techniques described on this page.

    Outputcombining_layouts.pycombining_layouts.tcss

    CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2585\u2585\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Header, Static\n\n\nclass CombiningLayoutsExample(App):\n    CSS_PATH = \"combining_layouts.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Container(id=\"app-grid\"):\n            with VerticalScroll(id=\"left-pane\"):\n                for number in range(15):\n                    yield Static(f\"Vertical layout, child {number}\")\n            with Horizontal(id=\"top-right\"):\n                yield Static(\"Horizontally\")\n                yield Static(\"Positioned\")\n                yield Static(\"Children\")\n                yield Static(\"Here\")\n            with Container(id=\"bottom-right\"):\n                yield Static(\"This\")\n                yield Static(\"panel\")\n                yield Static(\"is\")\n                yield Static(\"using\")\n                yield Static(\"grid layout!\", id=\"bottom-right-final\")\n\n\nif __name__ == \"__main__\":\n    app = CombiningLayoutsExample()\n    app.run()\n
    #app-grid {\n    layout: grid;\n    grid-size: 2;  /* two columns */\n    grid-columns: 1fr;\n    grid-rows: 1fr;\n}\n\n#left-pane > Static {\n    background: $boost;\n    color: auto;\n    margin-bottom: 1;\n    padding: 1;\n}\n\n#left-pane {\n    width: 100%;\n    height: 100%;\n    row-span: 2;\n    background: $panel;\n    border: dodgerblue;\n}\n\n#top-right {\n    height: 100%;\n    background: $panel;\n    border: mediumvioletred;\n}\n\n#top-right > Static {\n    width: auto;\n    height: 100%;\n    margin-right: 1;\n    background: $boost;\n}\n\n#bottom-right {\n    height: 100%;\n    layout: grid;\n    grid-size: 3;\n    grid-columns: 1fr;\n    grid-rows: 1fr;\n    grid-gutter: 1;\n    background: $panel;\n    border: greenyellow;\n}\n\n#bottom-right-final {\n    column-span: 2;\n}\n\n#bottom-right > Static {\n    height: 100%;\n    background: $boost;\n}\n

    Textual layouts make it easy to design and build real-life applications with relatively little code.

    "},{"location":"guide/queries/","title":"DOM Queries","text":"

    In the previous chapter we introduced the DOM which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS selectors.

    Selectors are a very useful idea and can do more than apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!

    "},{"location":"guide/queries/#query-one","title":"Query one","text":"

    The query_one method gets a single widget in an app or other widget. If you call it with a selector it will return the first matching widget.

    Let's say we have a widget with an ID of send and we want to get a reference to it in our app. We could do this with the following:

    send_button = self.query_one(\"#send\")\n

    If there is no widget with an ID of send, Textual will raise a NoMatches exception. Otherwise it will return the matched widget.

    You can also add a second parameter for the expected type.

    send_button = self.query_one(\"#send\", Button)\n

    If the matched widget is not a button (i.e. if isinstance(widget, Button) equals False), Textual will raise a WrongType exception.

    Tip

    The second parameter allows type-checkers like MyPy to know the exact return type. Without it, MyPy would only know the result of query_one is a Widget (the base class).

    "},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

    Apps and widgets have a query method which finds (or queries) widgets. This method returns a DOMQuery object which is a list-like container of widgets.

    If you call query with no arguments, you will get back a DOMQuery containing all widgets. This method is recursive, meaning it will also return child widgets (as many levels as required).

    Here's how you might iterate over all the widgets in your app:

    for widget in self.query():\n    print(widget)\n

    Called on the app, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.

    Note

    All the query and related methods work on both App and Widget sub-classes.

    "},{"location":"guide/queries/#query-selectors","title":"Query selectors","text":"

    You can call query with a CSS selector. Let's look a few examples:

    If we want to find all the button widgets, we could do something like the following:

    for button in self.query(\"Button\"):\n    print(button)\n

    Any selector that works in CSS will work with the query method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:

    for button in self.query(\"Dialog Button.disabled\"):\n    print(button)\n

    Info

    The selector Dialog Button.disabled says find all the Button with a CSS class of disabled that are a child of a Dialog widget.

    "},{"location":"guide/queries/#results","title":"Results","text":"

    Query objects have a results method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.

    The following example queries for widgets with the disabled CSS class and iterates over just the Button objects.

    for button in self.query(\".disabled\").results(Button):\n    print(button)\n

    Tip

    This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that button is a Widget (the base class).

    "},{"location":"guide/queries/#query-objects","title":"Query objects","text":"

    We've seen that the query method returns a DOMQuery object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as query[0], len(query) ,reverse(query) etc). They also have a number of other methods to simplify retrieving and modifying widgets.

    "},{"location":"guide/queries/#first-and-last","title":"First and last","text":"

    The first and last methods return the first or last matching widget from the selector, respectively.

    Here's how we might find the last button in an app:

    last_button = self.query(\"Button\").last()\n

    If there are no buttons, Textual will raise a NoMatches exception. Otherwise it will return a button widget.

    Both first() and last() accept an expect_type argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class .disabled, and we want to check it really is a button. We could do this:

    disabled_button = self.query(\".disabled\").last(Button)\n

    The query selects all widgets with a disabled CSS class. The last method gets the last disabled widget and checks it is a Button and not any other kind of widget.

    If the last widget is not a button, Textual will raise a WrongType exception.

    Tip

    Specifying the expected type allows type-checkers like MyPy to know the exact return type.

    "},{"location":"guide/queries/#filter","title":"Filter","text":"

    Query objects have a filter method which further refines a query. This method will return a new query object with widgets that match both the original query and the new selector.

    Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:

    # Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Buttons with 'disabled' CSS class\ndisabled_buttons = buttons_query.filter(\".disabled\")\n

    Iterating over disabled_buttons will give us all the disabled buttons.

    "},{"location":"guide/queries/#exclude","title":"Exclude","text":"

    Query objects have an exclude method which is the logical opposite of filter. The exclude method removes any widgets from the query object which match a selector.

    Here's how we could get all the buttons which don't have the disabled class set.

    # Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Remove all the Buttons with the 'disabled' CSS class\nenabled_buttons = buttons_query.exclude(\".disabled\")\n
    "},{"location":"guide/queries/#loop-free-operations","title":"Loop-free operations","text":"

    Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.

    For instance, let's say we want to disable all buttons in an app. We could do this by calling add_class() on a query object.

    self.query(\"Button\").add_class(\"disabled\")\n

    This single line is equivalent to the following:

    for widget in self.query(\"Button\"):\n    widget.add_class(\"disabled\")\n

    Here are the other loop-free methods on query objects:

    • set_class Sets a CSS class (or classes) on matched widgets.
    • add_class Adds a CSS class (or classes) to matched widgets.
    • remove_class Removes a CSS class (or classes) from matched widgets.
    • toggle_class Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
    • remove Removes matched widgets from the DOM.
    • refresh Refreshes matched widgets.
    "},{"location":"guide/reactivity/","title":"Reactivity","text":"

    Textual's reactive attributes are attributes with superpowers. In this chapter we will look at how reactive attributes can simplify your apps.

    Quote

    With great power comes great responsibility.

    \u2014 Uncle Ben

    "},{"location":"guide/reactivity/#reactive-attributes","title":"Reactive attributes","text":"

    Textual provides an alternative way of adding attributes to your widget or App, which doesn't require adding them to your class constructor (__init__). To create these attributes import reactive from textual.reactive, and assign them in the class scope.

    The following code illustrates how to create reactive attributes:

    from textual.reactive import reactive\nfrom textual.widget import Widget\n\nclass Reactive(Widget):\n\n    name = reactive(\"Paul\")  # (1)!\n    count = reactive(0) # (2)!\n    is_cool = reactive(True)  # (3)!\n
    1. Create a string attribute with a default of \"Paul\"
    2. Creates an integer attribute with a default of 0.
    3. Creates a boolean attribute with a default of True.

    The reactive constructor accepts a default value as the first positional argument.

    Information

    Textual uses Python's descriptor protocol to create reactive attributes, which is the same protocol used by the builtin property decorator.

    You can get and set these attributes in the same way as if you had assigned them in an __init__ method. For instance self.name = \"Jessica\", self.count += 1, or print(self.is_cool).

    "},{"location":"guide/reactivity/#dynamic-defaults","title":"Dynamic defaults","text":"

    You can also set the default to a function (or other callable). Textual will call this function to get the default value. The following code illustrates a reactive value which will be automatically assigned the current time when the widget is created:

    from time import time\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\n\nclass Timer(Widget):\n\n    start_time = reactive(time)  # (1)!\n
    1. The time function returns the current time in seconds.
    "},{"location":"guide/reactivity/#typing-reactive-attributes","title":"Typing reactive attributes","text":"

    There is no need to specify a type hint if a reactive attribute has a default value, as type checkers will assume the attribute is the same type as the default.

    You may want to add explicit type hints if the attribute type is a superset of the default type. For instance if you want to make an attribute optional. Here's how you would create a reactive string attribute which may be None:

        name: reactive[str | None] = reactive(\"Paul\")\n
    "},{"location":"guide/reactivity/#smart-refresh","title":"Smart refresh","text":"

    The first superpower we will look at is \"smart refresh\". When you modify a reactive attribute, Textual will make note of the fact that it has changed and refresh automatically.

    Information

    If you modify multiple reactive attributes, Textual will only do a single refresh to minimize updates.

    Let's look at an example which illustrates this. In the following app, the value of an input is used to update a \"Hello, World!\" type greeting.

    refresh01.pyrefresh01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\")\n\n    def render(self) -> str:\n        return f\"Hello, {self.who}!\"\n\n\nclass WatchApp(App):\n    CSS_PATH = \"refresh01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter your name\")\n        yield Name()\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        self.query_one(Name).who = event.value\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\nName {\n    height: 100%;\n    content-align: center middle;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aTextual\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Hello,\u00a0Textual!

    The Name widget has a reactive who attribute. When the app modifies that attribute, a refresh happens automatically.

    Information

    Textual will check if a value has really changed, so assigning the same value wont prompt an unnecessary refresh.

    "},{"location":"guide/reactivity/#disabling-refresh","title":"Disabling refresh","text":"

    If you don't want an attribute to prompt a refresh or layout but you still want other reactive superpowers, you can use var to create an attribute. You can import var from textual.reactive.

    The following code illustrates how you create non-refreshing reactive attributes.

    class MyWidget(Widget):\n    count = var(0)  # (1)!\n
    1. Changing self.count wont cause a refresh or layout.
    "},{"location":"guide/reactivity/#layout","title":"Layout","text":"

    The smart refresh feature will update the content area of a widget, but will not change its size. If modifying an attribute should change the size of the widget, you can set layout=True on the reactive attribute. This ensures that your CSS layout will update accordingly.

    The following example modifies \"refresh01.py\" so that the greeting has an automatic width.

    refresh02.pyrefresh02.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\", layout=True)  # (1)!\n\n    def render(self) -> str:\n        return f\"Hello, {self.who}!\"\n\n\nclass WatchApp(App):\n    CSS_PATH = \"refresh02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter your name\")\n        yield Name()\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        self.query_one(Name).who = event.value\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    1. This attribute will update the layout when changed.
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\nName {\n    width: auto;\n    height: auto;\n    border: heavy $secondary;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aname\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Hello,\u00a0name!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    If you type in to the input now, the greeting will expand to fit the content. If you were to set layout=False on the reactive attribute, you should see that the box remains the same size when you type.

    "},{"location":"guide/reactivity/#validation","title":"Validation","text":"

    The next superpower we will look at is validation, which can check and potentially modify a value you assign to a reactive attribute.

    If you add a method that begins with validate_ followed by the name of your attribute, it will be called when you assign a value to that attribute. This method should accept the incoming value as a positional argument, and return the value to set (which may be the same or a different value).

    A common use for this is to restrict numbers to a given range. The following example keeps a count. There is a button to increase the count, and a button to decrease it. The validation ensures that the count will never go above 10 or below zero.

    validate01.pyvalidate01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, RichLog\n\n\nclass ValidateApp(App):\n    CSS_PATH = \"validate01.tcss\"\n\n    count = reactive(0)\n\n    def validate_count(self, count: int) -> int:\n        \"\"\"Validate value.\"\"\"\n        if count < 0:\n            count = 0\n        elif count > 10:\n            count = 10\n        return count\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Button(\"+1\", id=\"plus\", variant=\"success\"),\n            Button(\"-1\", id=\"minus\", variant=\"error\"),\n            id=\"buttons\",\n        )\n        yield RichLog(highlight=True)\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"plus\":\n            self.count += 1\n        else:\n            self.count -= 1\n        self.query_one(RichLog).write(f\"count = {self.count}\")\n\n\nif __name__ == \"__main__\":\n    app = ValidateApp()\n    app.run()\n
    #buttons {\n    dock: top;\n    height: auto;\n}\n

    ValidateApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 +1-1 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    If you click the buttons in the above example it will show the current count. When self.count is modified in the button handler, Textual runs validate_count which performs the validation to limit the value of count.

    "},{"location":"guide/reactivity/#watch-methods","title":"Watch methods","text":"

    Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch method names begin with watch_ followed by the name of the attribute, and should accept one or two arguments. If the method accepts a single argument, it will be called with the new assigned value. If the method accepts two positional arguments, it will be called with both the old value and the new value.

    The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example \"darkorchid\" or \"#52de44\".

    watch01.pywatch01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.color import Color, ColorParseError\nfrom textual.containers import Grid\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\n\n\nclass WatchApp(App):\n    CSS_PATH = \"watch01.tcss\"\n\n    color = reactive(Color.parse(\"transparent\"))  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a color\")\n        yield Grid(Static(id=\"old\"), Static(id=\"new\"), id=\"colors\")\n\n    def watch_color(self, old_color: Color, new_color: Color) -> None:  # (2)!\n        self.query_one(\"#old\").styles.background = old_color\n        self.query_one(\"#new\").styles.background = new_color\n\n    def on_input_submitted(self, event: Input.Submitted) -> None:\n        try:\n            input_color = Color.parse(event.value)\n        except ColorParseError:\n            pass\n        else:\n            self.query_one(Input).value = \"\"\n            self.color = input_color  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = WatchApp()\n    app.run()\n
    1. Creates a reactive color attribute.
    2. Called when self.color is changed.
    3. New color is assigned here.
    Input {\n    dock: top;\n    margin-top: 1;\n}\n\n#colors {\n    grid-size: 2 1;\n    grid-gutter: 2 4;\n    grid-columns: 1fr;\n    margin: 0 1;\n}\n\n#old {\n    height: 100%;\n    border: wide $secondary;\n}\n\n#new {\n    height: 100%;\n    border: wide $secondary;\n}\n

    WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adarkorchid\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    The color is parsed in on_input_submitted and assigned to self.color. Because color is reactive, Textual also calls watch_color with the old and new values.

    "},{"location":"guide/reactivity/#when-are-watch-methods-called","title":"When are watch methods called?","text":"

    Textual only calls watch methods if the value of a reactive attribute changes. If the newly assigned value is the same as the previous value, the watch method is not called. You can override this behaviour by passing always_update=True to reactive.

    "},{"location":"guide/reactivity/#dynamically-watching-reactive-attributes","title":"Dynamically watching reactive attributes","text":"

    You can programmatically add watchers to reactive attributes with the method watch. This is useful when you want to react to changes to reactive attributes for which you can't edit the watch methods.

    The example below shows a widget Counter that defines a reactive attribute counter. The app that uses Counter uses the method watch to keep its progress bar synced with the reactive attribute:

    dynamic_watch.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Button, Label, ProgressBar\n\n\nclass Counter(Widget):\n    DEFAULT_CSS = \"Counter { height: auto; }\"\n    counter = reactive(0)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Label()\n        yield Button(\"+10\")\n\n    def on_button_pressed(self) -> None:\n        self.counter += 10\n\n    def watch_counter(self, counter_value: int):\n        self.query_one(Label).update(str(counter_value))\n\n\nclass WatchApp(App[None]):\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield ProgressBar(total=100, show_eta=False)\n\n    def on_mount(self):\n        def update_progress(counter_value: int):  # (2)!\n            self.query_one(ProgressBar).update(progress=counter_value)\n\n        self.watch(self.query_one(Counter), \"counter\", update_progress)  # (3)!\n\n\nif __name__ == \"__main__\":\n    WatchApp().run()\n
    1. counter is a reactive attribute defined inside Counter.
    2. update_progress is a custom callback that will update the progress bar when counter changes.
    3. We use the method watch to set update_progress as an additional watcher for the reactive attribute counter.

    WatchApp 30 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 +10 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250130%

    "},{"location":"guide/reactivity/#compute-methods","title":"Compute methods","text":"

    Compute methods are the final superpower offered by the reactive descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with compute_ followed by the name of the reactive value.

    You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.

    The following example uses a computed attribute. It displays three inputs for each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.

    computed01.pycomputed01.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\n\n\nclass ComputedApp(App):\n    CSS_PATH = \"computed01.tcss\"\n\n    red = reactive(0)\n    green = reactive(0)\n    blue = reactive(0)\n    color = reactive(Color.parse(\"transparent\"))\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            Input(\"0\", placeholder=\"Enter red 0-255\", id=\"red\"),\n            Input(\"0\", placeholder=\"Enter green 0-255\", id=\"green\"),\n            Input(\"0\", placeholder=\"Enter blue 0-255\", id=\"blue\"),\n            id=\"color-inputs\",\n        )\n        yield Static(id=\"color\")\n\n    def compute_color(self) -> Color:  # (1)!\n        return Color(self.red, self.green, self.blue).clamped\n\n    def watch_color(self, color: Color) -> None:  # (2)\n        self.query_one(\"#color\").styles.background = color\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        try:\n            component = int(event.value)\n        except ValueError:\n            self.bell()\n        else:\n            if event.input.id == \"red\":\n                self.red = component\n            elif event.input.id == \"green\":\n                self.green = component\n            else:\n                self.blue = component\n\n\nif __name__ == \"__main__\":\n    app = ComputedApp()\n    app.run()\n
    1. Combines color components in to a Color object.
    2. The watch method is called when the result of compute_color changes.
    #color-inputs {\n    dock: top;\n    height: auto;\n}\n\nInput {\n    width: 1fr;\n}\n\n#color {\n    height: 100%;\n    border: tall $secondary;\n}\n

    ComputedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a0\u258e\u258a0\u258e\u258a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the compute_color method which combines the color components into a Color object. It will be recalculated when any of the red , green, or blue attributes are modified.

    When the result of compute_color changes, Textual will also call watch_color since color still has the watch method superpower.

    Note

    Textual will first attempt to call the compute method for a reactive attribute, followed by the validate method, and finally the watch method.

    Note

    It is best to avoid doing anything slow or CPU-intensive in a compute method. Textual calls compute methods on an object when any reactive attribute changes.

    "},{"location":"guide/screens/","title":"Screens","text":"

    This chapter covers Textual's screen API. We will discuss how to create screens and switch between them.

    "},{"location":"guide/screens/#what-is-a-screen","title":"What is a screen?","text":"

    Screens are containers for widgets that occupy the dimensions of your terminal. There can be many screens in a given app, but only one screen is active at a time.

    Textual requires that there be at least one screen object and will create one implicitly in the App class. If you don't change the screen, any widgets you mount or compose will be added to this default screen.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()"},{"location":"guide/screens/#creating-a-screen","title":"Creating a screen","text":"

    You can create a screen by extending the Screen class which you can import from textual.screen. The screen may be styled in the same way as other widgets, with the exception that you can't modify the screen's dimensions (as these will always be the size of your terminal).

    Let's look at a simple example of writing a screen class to simulate Window's blue screen of death.

    screen01.pyscreen01.tcssOutput screen01.py
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\n\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\n\nPress Enter to return to Windows, or\n\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\n\nError: 0E : 016F : BFF9B3D4\n\"\"\"\n\n\nclass BSOD(Screen):\n    BINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\n\n    def compose(self) -> ComposeResult:\n        yield Static(\" Windows \", id=\"title\")\n        yield Static(ERROR_TEXT)\n        yield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\n\n\nclass BSODApp(App):\n    CSS_PATH = \"screen01.tcss\"\n    SCREENS = {\"bsod\": BSOD()}\n    BINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\n\n\nif __name__ == \"__main__\":\n    app = BSODApp()\n    app.run()\n
    screen01.tcss
    BSOD {\n    align: center middle;\n    background: blue;\n    color: white;\n}\n\nBSOD>Static {\n    width: 70;\n}\n\n#title {\n    content-align-horizontal: center;\n    text-style: reverse;\n}\n\n#any-key {\n    content-align-horizontal: center;\n}\n

    BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

    If you run this you will see an empty screen. Hit the B key to show a blue screen of death. Hit Esc to return to the default screen.

    The BSOD class above defines a screen with a key binding and compose method. These should be familiar as they work in the same way as apps.

    The app class has a new SCREENS class variable. Textual uses this class variable to associate a name with screen object (the name is used to reference screens in the screen API). Also in the app is a key binding associated with the action \"push_screen('bsod')\". The screen class has a similar action \"pop_screen\" bound to the Esc key. We will cover these actions below.

    "},{"location":"guide/screens/#named-screens","title":"Named screens","text":"

    You can associate a screen with a name by defining a SCREENS class variable in your app, which should be a dict that maps names on to Screen objects. The name of the screen may be used interchangeably with screen objects in much of the screen API.

    You can also install new named screens dynamically with the install_screen method. The following example installs the BSOD screen in a mount handler rather than from the SCREENS variable.

    screen02.pyscreen02.tcssOutput screen02.py
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\n\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\n\nPress Enter to return to Windows, or\n\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\n\nError: 0E : 016F : BFF9B3D4\n\"\"\"\n\n\nclass BSOD(Screen):\n    BINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\n\n    def compose(self) -> ComposeResult:\n        yield Static(\" Windows \", id=\"title\")\n        yield Static(ERROR_TEXT)\n        yield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\n\n\nclass BSODApp(App):\n    CSS_PATH = \"screen02.tcss\"\n    BINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\n\n    def on_mount(self) -> None:\n        self.install_screen(BSOD(), name=\"bsod\")\n\n\nif __name__ == \"__main__\":\n    app = BSODApp()\n    app.run()\n
    screen02.tcss
    BSOD {\n    align: center middle;\n    background: blue;\n    color: white;\n}\n\nBSOD>Static {\n    width: 70;\n}\n\n#title {\n    content-align-horizontal: center;\n    text-style: reverse;\n}\n\n#any-key {\n    content-align-horizontal: center;\n}\n

    BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

    Although both do the same thing, we recommend SCREENS for screens that exist for the lifetime of your app.

    "},{"location":"guide/screens/#uninstalling-screens","title":"Uninstalling screens","text":"

    Screens defined in SCREENS or added with install_screen are installed screens. Textual will keep these screens in memory for the lifetime of your app.

    If you have installed a screen, but you later want it to be removed and cleaned up, you can call uninstall_screen.

    "},{"location":"guide/screens/#screen-stack","title":"Screen stack","text":"

    Textual apps keep a stack of screens. You can think of this screen stack as a stack of paper, where only the very top sheet is visible. If you remove the top sheet, the paper underneath becomes visible. Screens work in a similar way.

    Note

    You can also make parts of the top screen translucent, so that deeper screens show through. See Screen opacity.

    The active screen (top of the stack) will render the screen and receive input events. The following API methods on the App class can manipulate this stack, and let you decide which screen the user can interact with.

    "},{"location":"guide/screens/#push-screen","title":"Push screen","text":"

    The push_screen method puts a screen on top of the stack and makes that screen active. You can call this method with the name of an installed screen, or a screen object.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVcXG1z2khcdTAwMTL+nl/h8n3Zq1xu2nnr6Zmturpcbk5cdTAwMWN7XHUwMDEzx/Fb/HK35ZJBgNaAMFx1MDAxMrbxVv779chcdTAwMGVcdTAwMTIvXCJgMEtyVGyMRkitmaeffrpnJn+92tjYTPqdYPO3jc3gvuI3w2rXv9t87Y7fXHUwMDA23TiM2tQk0s9x1OtW0jNcdTAwMWJJ0ol/+/XXlt+9XHUwMDBlkk7Tr1x1MDAwNN5tXHUwMDE49/xmnPSqYeRVotavYVx1MDAxMrTif7vfn/xW8K9O1KomXS+7SSmohknUfbxX0FxmWkE7ienq/6HPXHUwMDFiXHUwMDFif6W/c9ZVQ79cdTAwMTW1q+npaUPOPLSjRz9F7dRUY5hgWqFcdTAwMWGcXHUwMDEwxm/pZklQpdZcdTAwMWFcdTAwMTlcdTAwMWNkLe7QJjQv9MPZWalafuj/WTqqmebJXHUwMDE3nt21XHUwMDE2NptHSb+Z2lx1MDAxNEf0KFlbnHSj6+A0rCZccmrlI8eLvtWNevVGO4jjoe9EXHUwMDFkv1x1MDAxMiZ9d4yxwVG/XU+vkVx1MDAxZLmnT4prjzFccmgtN1x1MDAwMCx7WPd9oYzHuFVWgFx1MDAxMlpxgSOGbUVNXHUwMDFhXHUwMDA2MuxcdTAwMWYsfWWmXfmV6zrZ165m53Dw/aua1tlZd09cdTAwMGasLHjKKIkwaGpcdTAwMDRhvZE4I4z2XGZyZvOtcZCOXHUwMDAyV1ZbhsCzXHUwMDE2d9PObjWFw1x1MDAxZqP92PC7naf+2ozdh5zBztZ3o1jK4yk30sFu9fz86Fg97Fx1MDAxZVx1MDAxZX5IToKqvdy6XHUwMDFmXFxrXGJ8frdcdTAwMWLdbVx1MDAwZVq+Pv2VmdbrVP1HTHGtlWVcdTAwMTJcZkeW9XQzbF9TY7vXbGbHosp1XHUwMDA2w/To19dzg19cdFVcdTAwMDR+Qo7Qmis5O/rvoHFwvV+PXHUwMDBmPnROXHK7XHUwMDEwd1dvP/TWXHUwMDFj/YJ5wjKu6FGNNiiH4c9Re1xujVx1MDAwNcGNsoh8MfjX/CvGYIng18BcdTAwMDRYo/lqwVx1MDAxZuvz8Kh8X98qb5v7crdzqe3t4VLAb1xmXHUwMDFhLkGwZYE/XHTuk0nIXHUwMDA3XHKFyDdGXHUwMDEx+Fx0XHUwMDBmMyNfto9iXHUwMDFktMzno3q3vHuO8mRfrDnvXHUwMDFiXHSe0VJoXHUwMDAzipxcdTAwMWPsXHUwMDEw8ClcdTAwMTB4TCgujaBcdTAwMWZccrBcdTAwMTDu0Vx1MDAwMquJcdxzZsZcdTAwMDGPfFxm5lx1MDAxNH6YlIrJn4fjhbZKSJhcdTAwMDPmXHUwMDE5mqJ2clx1MDAxND5cdTAwMDQpOVxmXHUwMDFk3fZbYbM/XHUwMDA0iVx1MDAxNP9k4FGlXHUwMDFiXHUwMDA07Vxy/t/2L42wWlxy2v/MXHUwMDBmWVx1MDAxY9D93Vx1MDAwNfXwN980w7rzls1mUFx1MDAxYnajJCQpNmhOolxcXHUwMDFmV8hcdTAwMTKfLtfdrY4+UdRccuth229cdTAwMWVcdTAwMTdb9Sxv1kZcdTAwMTR7M1xigUTgXHUwMDE5/L/nzVx1MDAxNyfbd35n//D+Zlx1MDAwN45bV+clXHUwMDE5Xa+5Nyu0XHUwMDFlSoZWUawg+squ4r5P4cVcdTAwMTDgkFx0ozSSWHpcdTAwMTFvXHUwMDE2MiPMgTfnjj15s2aS5FxymiyU/uAxy1x0Nupblj3pipxZbPxCiVN41VxmJjuzgKFvrsiZ81ZNdebHbp7gzVxc5eLCqDtTTFKkj0Xm8N9z5+kjP4c7i1Fsvpg7g9RcdTAwMWVcdTAwMThtKTZLhZSaXHKrUqU8y8iVXHUwMDA1Q2lAczli2HL8XHUwMDE50JOcXiR/XHUwMDE13Vx1MDAwYsxcdTAwMDT3RuY5cVxmkqhF0082bt9it6A2inXyXHUwMDE5+Vlq51x1MDAxNHf/nkPOk0Hl7PC7STlsV8N2nVx1MDAxYTMm+VZm2J0hRqQuXFzpOSuZp4FzSYNI6Vx1MDAwNVx09ixRdV3hd5zN0mOGRFx1MDAwZadMg34o6Xo64+vAqqBd/b5N0/OvIZtcdTAwMTgpXFxOXHRccpL2s1xcXHQ7Zlx1MDAxNNmkZZrzcCYkmT5mU9OPk62o1VxuXHUwMDEz6vrPUdhORrs47cs3zs1cdTAwMWKBP8ZcdTAwMWb0TPm2UT7ouCtcdTAwMGXTevbXRuYw6YfB33+8nnh2MZbda1xmxdnlXuXf52Yyul0hkZGsZsRcdTAwMDSQucz3iGy6XHUwMDFlXUtcIjPUtYpcdTAwMGLrKkdEMyp72DTLkOhRzoeugEN5iFUjdi2HxzR6hmv6p4GowfKMTFx1MDAwNzRmXGJcdTAwMDCaXHUwMDEyaVdH4qBzicZcdTAwMTONSUtcdTAwMDJcdTAwMTJgxSSGXCLPqMsnselp61x1MDAxMGFI5ViCXHUwMDEzYl1HWZM76YnEhCdcdTAwMTEoWilwWog/l8Sml1BzNpXIKGlpXFy4pZtcdTAwMDGiVuNGecagdIqBMkgk++GHZrFSIZTT1jFcdTAwMTTPSWNTXG6F0rDRo1x1MDAwM1wi05K7bFx1MDAxNmZPsPzfXHUwMDBm3vfffLx4+yG86PcvL9tcdTAwMGbNXHUwMDAztt5cdFx1MDAxNiFcdTAwMWY8XHUwMDA1oDSjIE3cnYmhtE5cdTAwMGXCQ8s1UlJPRJZcdTAwMTNsa1ImJ7BokCBeoExeXFzLI6fjdq5cIsdz8Vx0qjBjUOT3zpjZq3kx6NPLclx1MDAxZuO9m/2y6uzU7vq4t/bwtJ6SXHUwMDAyXFzh0lx1MDAwMojhQMuN8Fx1MDAxOKN2XHUwMDEyQYyyXG47qlx1MDAwMP7eOjblXGJcdTAwMDBMw1x1MDAwYpSxp1TgrFwiPWJXXHUwMDAwTi1kXHUwMDExOEFKrTnD2UXgVrVcdTAwMWQlW5c7+zbeerhcdTAwMGXOeVJcdTAwMGab61x1MDAwZU4hPFLXRlJ4XHUwMDAyru1wdaqktYdcdTAwMTTTSGExJPDgYjLQiIrlwVx1MDAxMqkzzcCZYSueYTRBsNP7+PDQvLrQlzWZhLJ/wXJCaCz/XHUwMDE4tHx9Pe26+/H+obi77iad29Ln2t71l/v34tNcdTAwMTKuW8Wbm+B3/Vx0opCfRNt7N3uldydLuO5N+fN+VZ2etu7Ptt9WoFx1MDAwYu/fXHUwMDA0/WVV4Y1GsHJZXHUwMDFjUFSelpJcdTAwMTdcdTAwMTFcdTAwMDBSVkHaWsqZXHTg0n93/mf/rnZcdTAwMTi/3T/dub1Oro72dtY7XHUwMDBipDOMR1x1MDAxOZ5kLolcdTAwMTJsZJa1RKmFJ4hcYkmju0lYu2BcIkhZ01UwQTxBbrZ74PpqzOGVm/cmmbdSqYRcYozPXHUwMDE1jbJcdTAwMTHPSsiUT3NDbKtJoXKj7dA5g4JyhrVvXHUwMDA1Zb/T8Tq9uHFcdTAwMTmnNdxfXHUwMDFl3+TksnKupL+KsnKhbVNdsbAkI6coRZe1XHUwMDAym10oTqe8Zbhi1Y9cdTAwMWLBsoOx9ijUSlxutIYh48PB2IBcdTAwMDecXHUwMDE5amTkhsYsNvFb5IncXHUwMDEzzsW04kZxd5dcdI7JuSRSsKBpRCwl72JsJom7iVx1MDAwMiZI187vqc8vy5BuJLvnWaCQs2Omssx0ibeRL8tYo5hQSlCqRfwhclXNp1xuXGJ4Wlxu5UaUXHUwMDEz/VEnPp1QUJVcdTAwMTl+ilx1MDAxZqg0UoyotHVcZkvZ9V7l3+emXHUwMDEzZVxuQztHkETtYlx1MDAwZXE/XeusKaFcdTAwMDBcdTAwMDePUkpcdEaCXCKVn027pISiSFkjXG5cdTAwMDDNgVx1MDAxOGexuapcIkJcdTAwMTFcdTAwMWVQnm+UIHfgUptcdFx1MDAxYZ9z7qFby6WsK7vz3ErHgdJHXHUwMDFhM3jWcqpF+IS6jWfG/I18UlwiQrGCuVVBjjGktpg765FQKHhcYkB3krbaMGV/UkIpRJR7jWNpWXxC2UAhnyhcdTAwMDZMXG4+R6F1eq63pnxcIjR6qJTRmlx1MDAxMiPQw3RcIoT1pFx1MDAxNVppXHUwMDFhXHUwMDE4o5larJJVLFAsY5xuT9lcbpOcTZj6pifxSEXRXHUwMDE1LKUsbv52lE9QXHUwMDExMii1y1x1MDAxYVZCJ0ipy0vOXHUwMDFhzSFPSJxJKy2iXHUwMDA2olxuXHUwMDFh1DE6sVx1MDAxZadcdTAwMGUkkyl40Kjit6nXn41OXG5cdTAwMDGVNo5BaU46mbbEu3ihK7FcdTAwMWKSaJqjMv7pTInTqFxcOjg9tlx1MDAwZqdYPWbnX2prXnwkXHTmKTBGK0U9j7lcdTAwMTn5lE8096hcdTAwMDeMJFVohFx1MDAxNHKx0sNLbHBAXHUwMDEy+EOZ2ErKj0dcdTAwMTf7b5p7R8FJr1x1MDAwM+XD6qfT3avP0bLKbujkRcbcL1h6l4XhVEmwXHUwMDFhQc++kKxxU2p3XHUwMDBmto7fn15Hl5dxq9xTbz+sO/qBMlx1MDAxZlx1MDAwNsg5PaxQo/tcdTAwMWIs86RbdUSpXHUwMDEx0zQq6zUvxLlgXHUwMDE0RC0+I4IuMDGElph4XHUwMDFlQf5cXHSiLlxctmyF0qTF5+Dmz7+fd9iX3bPem/P6Wb9S7d10+s8qRq1cdTAwMTSdgjRcdTAwMDJ1ttRcdTAwMWElz3XH41x1MDAwNZRcdTAwMDdcXKBcdTAwMTKkXHUwMDExXHUwMDAw0C6WOy59Yki4WXVjV7wrYYF5oe9cdTAwMTKzJI3CzNKIuWg+RPDiJMcyRVx1MDAwMlxi7ezbzirJznZ8dVx1MDAxYpr7sniItpvxzVFcdTAwMTfWPckxaDw3XHUwMDExQsLEas1gWJWUXHUwMDA0+Vx1MDAwNaBUaaqthF5s31lcdTAwMTHy81x1MDAxYlCmrNjnXGYkokSxYqSrOHp7v7d3wpPjg6D8caeCeLK/XHUwMDE0pFPmzjVH8ayqy1wiS/blWi7Zl1x1MDAwYi/ZXHUwMDA3LC6DOpaXgs1cdTAwMTHKplx1MDAwZvx6TnFazTyBXHUwMDAyJMFcbpRcdTAwMWTZRq09bi0pXHUwMDE5Zlx1MDAwNXk7vkwgk5LuopVA6TJcdTAwMWGYtFx1MDAxZMcqz1xiSomsUW5cdTAwMTNcdTAwMGVcdTAwMWL3dVx1MDAwMaBcZua3XHUwMDFmr2Kp69xxJ2fHTEWL6UFiI1+0oFx1MDAwNF1cdTAwMGLLSSy7moTiuZNcdTAwMDZLXVx1MDAwNWjDnO3I3L7EpzN+tqJFqVx1MDAxMFHuNYal7HKv8u9zq1x1MDAwM1moio3klKHjXHUwMDFje/mOXHUwMDBm3vdKeHi6/eXjx/pd/fL0T41FS01cdTAwMWJ+pdHrXHUwMDA266CL0Vx1MDAxNYRAWMNcdTAwMTQoPZK1XHUwMDAxqWYrgCSElJbU68uIg9zE1jRtXHUwMDAwlMhz58Gr1Vx1MDAwNrUusWjnXGaO3zW2XHUwMDBmv1x1MDAxY4ja4bugM5s2eD3tui9a9rDWTZCtTHM8bqldXHUwMDA3nfFkyfO0XHUwMDA1isL/ocWme+D4XHUwMDFjM6zTcTNcdTAwMTdcdTAwMWasUFxcaItcdTAwMWVBR9Cjulx1MDAxZPt2ZFx1MDAxYo1cdTAwMTJcdTAwMWVoXHUwMDA13M2aUFx1MDAxZbtYXHUwMDExpzBZXHUwMDAwj9PFXHLnkrJxJifJXHUwMDBiro1cdTAwMDeIRksrXHUwMDA1XHUwMDEy6Mc20qCbu2GYXHUwMDFisZXMiTzb8WaUXHUwMDE308PMxvCuXHUwMDE1t0NDS+pAt0JcdTAwMDAmLNrgdJLmMt1qwMBcdTAwMDD8rFx1MDAwMqNcdTAwMThT7lVcdTAwMWGH05xcdTAwMTKjkFM0K+RcdTAwMTTQbjGylLNrjOkxY105XHUwMDA1085329qY0E5VjXCKJVx1MDAwNWKYNcYoXHUwMDE0L7Qm2yjPpYaoLaOsg7p9nFLIXHUwMDBlt1KNJCjRXHUwMDFiSW4xXoejhEpxQlHWef9/nFwiaFx1MDAxOK2rXHUwMDE4XHUwMDE56lx1MDAwYs3HOUW6rXCIlIQq1EaQfpzOKUVWTZ9cdTAwMDJcdTAwMWOxijFlJLOKI1GZlVx1MDAxM5jOc5UwTckxxVx1MDAwNUvZxI+9Qa9cdTAwMTDP7lVcdTAwMWGHclx1MDAxMZ29erqDW/x6lFx1MDAxMO5cdTAwMDZcdTAwMDNC0Fx1MDAwZatPSjB7zM3bMLgrT5qRSV9OeKVcdTAwMWTquChwXHUwMDBm+9fXV1//XHUwMDA3mNhRliJ9 Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden"},{"location":"guide/screens/#action","title":"Action","text":"

    You can also push screens with the \"app.push_screen\" action, which requires the name of an installed screen.

    "},{"location":"guide/screens/#pop-screen","title":"Pop screen","text":"

    The pop_screen method removes the top-most screen from the stack, and makes the new top screen active.

    Note

    The screen stack must always have at least one screen. If you attempt to remove the last screen, Textual will raise a ScreenStackError exception.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

    When you pop a screen it will be removed and deleted unless it has been installed or there is another copy of the screen on the stack.

    "},{"location":"guide/screens/#action_1","title":"Action","text":"

    You can also pop screens with the \"app.pop_screen\" action.

    "},{"location":"guide/screens/#switch-screen","title":"Switch screen","text":"

    The switch_screen method replaces the top of the stack with a new screen.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXG1T20hcdTAwMTL+nl9BcV/2qsLsTPe8btXVXHUwMDE1XHUwMDAxNlx1MDAwMVx1MDAxMpNccuH17opcdTAwMTK2bGsxsrFlXGZs5b9fjyCWLL9gg3DIKlXY1sjj1szTTz/dM8pfb1ZWVpPbTrj628pqeFNcclpRrVx1MDAxYlxmVt/689dht1x1MDAxN7VjaoL0c6/d71bTK5tJ0un99uuvl0H3XCJMOq2gXHUwMDFhsuuo11x1MDAwZlq9pF+L2qzavvw1SsLL3r/930pwXHUwMDE5/qvTvqwlXZb9yFpYi5J29/63wlZ4XHUwMDE5xkmPev9cdTAwMGZ9Xln5K/2bs65cdTAwMTZcdTAwMDWX7biWXp425MwzUDxbacepqWiElFxiKrsg6m3SjyVhjVrrZHCYtfhTq7fHrZPex6uT643+2ra4qrq7KKlmv1qPWq395LaV2tRr061kbb2k275cYo+iWtKkVlE4P+1b3Xa/0YzDXm/kO+1OUI2SWzqn+PBkXHUwMDEwN9IusjM39GlccpVgxmgrpOJSKG3VsN13IDUyY41RXHUwMDAypUGJQlx1MDAxNSzbaLdoXHUwMDFlyLJ/8PTIbDtcdTAwMGaqXHUwMDE3XHIyMK5l11xiXHUwMDE1XHUwMDA05/XsmsHD/UqnmLRcdTAwMTJN1n0zjFx1MDAxYc3Ez5DVzFx1MDAxYcFdvrVcdTAwMTemk1x1MDAwMCA5OpR22OB/sbNdS8Hwv+IoNoNu52G0Vnv+Q85ab+hWXHUwMDExSXk05eY5TCpR2N89QLt7uub0detcZuKPw75GoFx1MDAxN3S77cHqsOXbw7vMtH6nXHUwMDE23CNKaC2tdU5cIupsMltRfEGNcb/Vys61q1x1MDAxN1x1MDAxOVxi07Pf3i5cZn2Jalx1MDAxYfSFXHUwMDExllx1MDAxYm5cdTAwMTTOjf0/wi+wKW/OKp9cdTAwMGU+XHUwMDFjXHLuTreCne0vP1x1MDAxMvuCP1x1MDAwZX5pmDJGguZcXKAwelx1MDAwNPvogKHlXHUwMDFjXGJfTlx1MDAxYsefh/16cM65Klx1MDAxMftkmHBOw5LB/2Vf7Z00vsRna/31w62d/atccn5cdTAwMTmUXHUwMDAyfsdRcO2kKVx1MDAwYvxJeJNMQr6yelx1MDAxYfKNdMid4PNcdTAwMDM/6H95t3n2Z3vzc3x4dHP8YaNcIlx1MDAwZVx1MDAwZl478J1hloNBXHUwMDAx3FhrR4FvhGJIzi+ERUW+XHUwMDAxz8K9cYrXYVx1MDAxY/eC23HAXHUwMDFiUYS5UVxuLU1cdCxcdTAwMTflL0fxXHUwMDFl5dKAxlx1MDAwNVCeoalcdTAwMWQn+9FdmHLDyNnfg8uodTtcdTAwMDKJXHUwMDE0/mTgfrVcdTAwMWKG8Yr4b/xLM6rVwvif+Vx1MDAxOeuF9Pu+Qz36zfVW1PDOstpcbuujXpREpMOGzUk7N8ZVsiSg7rrbteJcdTAwMWS1u1EjioPW1+lWPc2ZXHUwMDAxi2eHYVxmOEVUoXF+bz4wx1f609ZBv9M53ZKbXHUwMDFmq+8/71x1MDAxZL52b7aOkXJDq1BobnKKNlxyYyiYoJNaodNKKlMwrFx1MDAxY29cdTAwMDbMOGTozblzXHUwMDBm3uxcZqCRSmZ38DeIWUpQzFi2N8PKL5Q2ReetcLI3g1x1MDAxYfnmkrw5b9VMb75cdTAwMWbmXHTuLCjgTPVnXHJcdTAwMTSXtMuJocf8efbML+DPxSD4gv5cZsYxdEpLZVx1MDAwNXJNideoQ2vrm41cdTAwMTDg6FJcdTAwMTC6YFo5XHUwMDFlrYk2hFBcdTAwMTKApsQ6nvHG0L9cdTAwMWRnXHUwMDE20GpDjsCdmaRRSVxccKKdJ0Tv1MxcdTAwMTn+PsMjlbJOLKJcInN2XHUwMDA03eRdXHUwMDE016K4QY1cdTAwMTmVfK8ybM9cdTAwMTElUlx1MDAxZq72vZWcXHTllPSJsybcWpVcdEs/XHUwMDE2QcdcdTAwMWLNQFhJk01aXmr/7+GKb0Ozwrj2uFGzM7CcUWucgeHcijTrp+R/kk2KQqexUjjpaO6tXHUwMDE5s6lcdTAwMTX0ko325WWU0Nh/bkdxUlx1MDAxY+N0MNe9ozfDYIxB6J7ybUVG6PhcdTAwMWVHiT17t5K5TPph+P5/bydePVx1MDAxNcv+XHUwMDE4Q3HW25v868JUZkFOT7BcdTAwMTUnW3CBPGO2XCJ9pUxmgUnUymjDSX7wQnXJSMtIXHUwMDBmWFx1MDAxNFx1MDAwMlx1MDAxMVxi/C9DZI5ZckJjuOFWTiQy5Vx1MDAxOGiN5H9cdTAwMDLISclZx5iMfIE6cFnDMojs6YnCnEQ2O3lcdTAwMWQhMs0taUhKXHRBccvR5i6651xmw1BrcjJCtsy50YIsNruGOmJcdTAwMTGXxPIkJo1cdTAwMTREU1x1MDAxM0js5+asabD1x9o4Ylx1MDAxN2StXHUwMDE5hUG0tnj2O29cdTAwMDHXXHUwMDE0yEDLjNlcdTAwMWXjrePT885e5Th+31x1MDAxZlxcRWu1TrR+dNF83Vx1MDAxOVx1MDAxNfi6oPWFXHUwMDExMFx1MDAwMimBzO72vihuXHUwMDE5+aZwzlx1MDAxMYNbmUsvn15cdTAwMTTXurzSoCNgSJJemdmlZVmPXHUwMDE1rrPg8nKFa6LGafjUToLBXFx6+Vx1MDAxODrXb+tcdTAwMDfBzqf1+LyyTTnCIN6CratS0VlcdTAwMGJ6zbBcXHgqYL5cXG2c4Vx1MDAwZYBcdTAwMTdcdTAwMTN+R0GXlI5cdTAwMDalwaJ2ZdStVYmVayFcdTAwMDWCUy+CzyFcdTAwMGJOqFx1MDAwMtTtYSVuXHUwMDFjbFx1MDAxY21tXZ9WKlx1MDAxZiuN3km/nCqAplx1MDAxMETiQC1cdTAwMDH9gtT4dFnprFEqP+WP0vOHs697h5+PzuqdXHUwMDAzPKq15Hn7Knnl9KzSlFx0lNdcdTAwMWFcblRu+S/twCGzaDSBTNNYQK75Kfi3UHVcIixz3caBXCI5vOxFS1x1MDAxYoZcdTAwMWb6XHUwMDFm7+5a56f6rI5JhLenfD70v53V77vbzul54/Bi1+3dbVx1MDAwZrqbUbR7Wi+h3+NrXHUwMDFi697B7mDQaZze1bu1qLr+R1x0/fbE+sGOXHUwMDFl3Fx1MDAwZWrrXCLYUdHazWkvLIdcdTAwMDW8KJDKubJYYFrJXHUwMDFi3dRcdTAwMDDo9Tl3Ml9Ee4xcdTAwMDFcdTAwMGX1p+jaXHUwMDFkh1eV3ubmn1j5uvm+rZ7CXHUwMDAwy0ssXHUwMDAxNVOCXCJcdTAwMWNcdTAwMTjKLa1Uo1x1MDAwYliCI6OMUyNpOGVcdTAwMTQ+s+ZccmDPw1x08kzpXHSppJxcdTAwMTDuaEpcdTAwMTRlTi9Q9J4lx5RxsFxiXHUwMDE0s1x1MDAxOc/K0mhcdTAwMTjZXHKgvVxmttqNXFwzLFJn+vd7kTrodFhvQJlb86yXVoZ/uX/BycXq3ELBMorVM6yb6Y/Ti9ZcdTAwMWPMNI90vvRHOkzM7ZCzma9cZod8XHRNqli6sMNcdH7kl06MeCRKzVxmSLBcXFhDI1JcdTAwMTRcdTAwMGLlOCQw37lUoFxyWIE4qdRjXHUwMDA1Q4da+1VlXHUwMDE0+dTtwVvRSikt8ieE5+dUeoBTQvlcdTAwMTRvnbPSM1vnreTrKs761X+Lylx1MDAxOcmlyVx1MDAxNVx1MDAxZVx1MDAxZVxuK4o5wf1FSFOluJFcdTAwMGZcdTAwMTdMKfWM3sVPVIKZjid/XHUwMDE0kZT19ib/+oRcdTAwMTWwnHuMhXdcdTAwMGUkLFx1MDAwNZ9f4M/WO6+TTYw0zO+70ki3i4iFXHUwMDA1MFx0TKVcdTAwMDOhTH7TVplUwpklia5IO5BzgsZcdFwi3zpcdTAwMDagUFJYXHUwMDE0xlx1MDAxYZlcdTAwMWKfXHUwMDA3Klx1MDAxMZo0XGKiWm7R2C836SetR5dNJWuCUTwgMVwiOWViXG5cdTAwMTByXHUwMDE33VOJZKTPNEpBI619reJvSiVrU1x1MDAwMeWPMSiVxiXcTS9cdTAwMTZcdTAwMTC1SePdaG4umZ3rvU4ukY5cdTAwMDaXK2JVXHUwMDFmzLRcdTAwMWRcdTAwMTUmNOSMa1x1MDAwZVx1MDAwNECjKVuXXHUwMDA1w8piXHUwMDEzpL7RXGItXHUwMDE1IV2bSUVcdTAwMDPHmfa0RmlcdTAwMDVcdTAwMDClNFx1MDAxM6SJcSgo21nyajrkPffHSlx1MDAxM6JcdTAwMTMgceaXRSjqar9vfTKlOO5cZo20X71cdTAwMWVfuv57UMpcZlD5Y1xmTlx1MDAwYnLKrJ3jZvqWO0cj75TUXHUwMDE57TxKKr/38fxz/Wj75FCfSLu373ai3dddgbSWM+VcdTAwMDR4tuZo5Gj5gSQzI6bn1ilcdTAwMDSH8nn7Z8tfXHUwMDFlks44Q1x1MDAxZbLkesSyloesnFx1MDAxZfJoSEg1gpj/mZ6dXHJcdTAwMGW7h4Ov8CFpVFx1MDAwNvWrvT+7W+9fOzolo2nXVlx1MDAwYidcdTAwMDGwXHUwMDEw8biXXCJCXHUwMDFiR6qMW8WfXHUwMDE38souj5NmJlx1MDAwNlEvUSxcdTAwMWJcdTAwMTLgXHUwMDEyq+NcdTAwMWLHXHUwMDFmZaRO63+Yg9v1rb3Eid8/XZRSbS7dpaZusJ7+iJyg8FwiXHSQXHUwMDBiXHUwMDE0t1x1MDAwME+qX9Q+bm13XHUwMDE0XHUwMDFjvW+a5ufaa5eQVvpcctbKbzJBa/Lrb2k6KvzOXHUwMDE3J8ibKFx1MDAxZlXuZfxcdCaloONcdTAwMWKshd/jrVx1MDAxNVx1MDAxN0t+XuLlcO6LY2ilWPrzXHUwMDEy+Cp3WOPzd1hLmPr4k/D79Ei32Pn3Jc6e+Ve5fOS0ZqBImVHex7WDwv5cdK2Ypmb/JIWw+efryvRnbVx1MDAxOGl30ipSS+B20sNQ1jFOXHUwMDEzXHUwMDAyXHUwMDAwXHUwMDBl1Eih68HXwVxuxV1+rW8ppeone+Oc+eDsXHUwMDEwMZJcdTAwMGZcbktJMeU3KFx1MDAxZFx0WpywXHUwMDA3XHUwMDEwaKa11ztaXHUwMDFhTjP+PVxyWnBb4mxcdTAwMTm4MrK5WlC+ZZVcdTAwMTbaot+GkD1dN7Rcblx1MDAxOKWupHGs81dKbse3fP9MmehULPujiOKsszf510VVicht8iqSXHUwMDE4aVx1MDAxMrA0vvPnoFti33aSRudT+6ZDaDpcYmrx2bspJNZcZqrNfjf88TpfkPDgQlxiIDCholxmf1ToXHUwMDFigcwh+ke+QPuc7zk8lnSDuNdcdLrkXHUwMDEwXHUwMDEztEkuxc20ybi058I/s/xcIlx1MDAwYuGztMmL7vuS4DRmkm9pT391w8v2dV7m/nBlktn0NF2SX+YperT0iSwp7Pk9evakL+TRS9zXXCItU9zHdK7AXHUwMDA3hsLzXHUwMDEyfl+LzzMoeZbGuGetos/0aFx1MDAwMaSB/Fx1MDAxNlNOXHUwMDFhSXOZ+48gsno1MFx1MDAwNc5vMjHoXHUwMDFmRCu6u0/wpXJmmdXq5/jjnOpkdqgoKFx1MDAwMUupo6LwbqSQOlx1MDAxYqJMnnCGXHUwMDE0XHUwMDE40Vx1MDAxMI+npamnqZPZu5hHXHUwMDE0XHUwMDEzd1xuaL6Ms1r6xYRxmyhqkDiRaK2jTFx1MDAwMIT6uZ/9mlx1MDAwZWV/rFx1MDAxNVE8TZ68efhcdTAwMDG/eWg/IcxccqeDYFx1MDAxZNVcdTAwMWWIPLvL1esoXHUwMDFjvJu0nzo9PEem4+mZKPT3+te3N9/+XHUwMDBmJe3LnyJ9 Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed

    Like pop_screen, if the screen being replaced is not installed it will be removed and deleted.

    "},{"location":"guide/screens/#action_2","title":"Action","text":"

    You can also switch screens with the \"app.switch_screen\" action which accepts the name of the screen to switch to.

    "},{"location":"guide/screens/#screen-opacity","title":"Screen opacity","text":"

    If a screen has a background color with an alpha component, then the background color will be blended with the screen beneath it. For example, if the top-most screen has a background set to rgba(0,0,255,0.5) then anywhere in the screen not occupied with a widget will display the second screen from the top, tinted with 50% blue.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVZ2VLjRlx1MDAxNH3nK1x1MDAxY+dlUlx1MDAwNZrel0mlUixhwizMXHUwMDAyXHUwMDE0Q5KplLDatsay5EhtMDPFv+dKXHUwMDE4tySvgJmFXHUwMDA3Y/dtt666zzn3XFz5y0aj0bRXXHUwMDAz03zWaJpRy4/CIPUvm5v5+IVJszCJIUSKz1kyTFvFzK61g+zZ06d9P+1cdTAwMTk7iPyW8S7CbOhHmVx1MDAxZFx1MDAwNmHitZL+09CafvZ7/nro981vg6RcdTAwMWbY1HNcdTAwMTfZMkFok/TmWiYyfVx1MDAxM9tcZlb/XHUwMDFiPjdcdTAwMWFfitdSdkHo95M4KKZcdTAwMTeBUnqK1kdcdTAwMGaTuEiVIEJcdTAwMTTmXHUwMDEyqcmMMNuDq1lcdTAwMTNAuFxyXHUwMDE5XHUwMDFiXHUwMDE3yYeaXHUwMDA3e6fPj7blaKsjRm/7r+WLbo+G7rLtMIqO7FVUJJUlcC8ultk06ZnTMLBdiOLa+Lxvpcmw041NllW+k1xm/FZor2CMo8mgXHUwMDFmd4ol3MhcYj5RwT2EhMKaXG4tKGVuO/LvXHUwMDBirDzKidKEUU0x4bW8dpNcYo5cdTAwMDHy+lx1MDAxOVx1MDAxNX8us3O/1etAenHg5mByrpRwcy7Hd8s095hiVLrluybsdG1xQMJTXHUwMDEyI12OZqY4XHUwMDAyrFx1MDAwNOFcYiniTii/5OAgKMDwsbxNcTDepnhcdTAwMThFLss88EdcdTAwMWRAZVx1MDAxMJVO99S+XHUwMDFh7n/OaHv44cxcdTAwMGbRXHQjvc7u5G4qiPPTNLlsTlwi1+N3LqPhIPBvcISF1Eoohlx1MDAxNEVu86Mw7tWTjZJWz0GvXHUwMDE4vd6cjXhrRnZcdTAwMTbctVx1MDAxNvPgzpXmikuyMti3tzvmg1x1MDAxY52/39k7XGbOXrB9/+jlm29cdHa1XGbskjOPYlx1MDAwZZCRhGvEZVx1MDAwNetKY48qXHUwMDA0jGBYYYJcdTAwMWaGdak5apNprONcdTAwMTIlJyDnuFx1MDAwZW2CqdSSUcF+cGhrJCnnWsg7QNthKIntUfj5Ro0ro/t+P4yuKkAoMFx1MDAwZlx07viZaWSt1Jj4n/jJwE9t6Edccqgx4XlkfimfWmYgl3zxkurlq2xHYSdnSzMy7SqNbFxiJWhcdTAwMTK2ycBFW5CVXHUwMDBmy6VcdTAwMDdB/e6SNOyEsVx1MDAxZlx1MDAxZK+W4UJm32z/XGZqY4llffiW21hJhDSidHV2L0bEXHUwMDFk2E1q4/dlN0ZL6S2FR6FqM62k4sLxt2C3VFDoXHUwMDE051goweRcdTAwMDMr2Vxcdlx1MDAwYk8rXHUwMDAypUpxRFx1MDAxMSup6YTrjIDKQFx1MDAwZZAoQpxxR/Ax9TlVipOy7VjOfEfpW6CQ8cj1fEFYUI1cdTAwMDT8x+w+lM0swHknjIMw7lRcdTAwMTNcdTAwMWL7tINcdTAwMTWKR0Hy1jDPclx1MDAwYnmMXHUwMDExTjCWXHUwMDFja02k5LQ0reNcdTAwMGbyrKlHJYg2lVx1MDAxNFx1MDAwYqZcdTAwMTGmU3dv4mB5Vov9Wy0rSYhcdTAwMDY0gUeUXHUwMDA03rNZWYGMc5goXHUwMDE54pzK6TOJ/MzuJv1+aGH73yZhbOvbXFzs53ZO+q7xp5RcdTAwMDXuqlx1MDAxY6urwyBfsSr+7l3D0af4MHn/cXPm7K254C6iU7h2622U/89cdTAwMTO2XHUwMDA1Jl2XqmDdpGMmKNdwXGYrK9v5q+10p3+xS+K93f1Pg97OSetcXH/nJl16YFx1MDAwZlx1MDAxNVx1MDAxMYJcdTAwMDFcdTAwMTFwzaRzykDaKEVcdTAwMWPioHO0ltfdpC2f0W6vz6QrTYG1uFR6XHUwMDFl08hkx1x1MDAwN0efgyx5d3T4qtc+XHUwMDE57tn/9vl6jFxmQUJAidGP7dFcdTAwMDWei3aMXHUwMDE1dDtSs9XR3rvAXHUwMDA3Q7/bZfzy9fut5+jw5cuT/lx1MDAxY7R3/VZ3mJrHxrtehnfCpKdcdTAwMTDCQlMmudBYVfDOoJTDXHUwMDE0qMFaXHUwMDAx9Vx1MDAxMWNcdTAwMGZcdTAwMDG8Tf04XHUwMDAzXHUwMDBmXHUwMDA26JpcdTAwMDY90WpcdTAwMWHt025cdTAwMWS6XHUwMDA1XHKtk6Lix1x1MDAwNzlXuX5/Nbd+nFxmtvpJZid+2Fx1MDAxZNGzRto595+gTbRJON9EXHUwMDFl/+XX78G+3zXl+/l5XVLMulxmXHUwMDEwiolAtESkZTKwXHUwMDE4MneSga9n6Fx1MDAxOZdcdTAwMWU4p7yJXHUwMDE0WKmyaS/qXHUwMDFlo56kXGaD+6JKXHUwMDEzVU9sfTJQ6Fx1MDAxMaRcdTAwMDCSxEB+S1x1MDAwZlx1MDAwZZytp55ASjFcZonkRdopwa1GIIrh4PSdXG7hWn19QW7KXHUwMDFm1dcvLjdVXHUwMDA3jYnWmGHwiphLrkt28tZAM3BcdTAwMWOC5s9cdTAwMWQpwlJSdT9bv9jxVZNC4HBcdTAwMTTO5VxcXG4o9nQ6K+VJXHUwMDAxglx1MDAwZolcdTAwMDMuOViCXHUwMDFm2tXPhXZcdTAwMTGsg/qOnr7Q51nahuY+hsR5h1x1MDAwMVxyk1hd20YjLsy74+fxv+/SreNR/81W/Fx1MDAxN/qWhn65slx0rj2Ru3VcZrZcdTAwMWSUy1GyeOqOmCeIgK6RsvyZ7MNcZn17lpsnTHlcdTAwMWFLaNhcdTAwMDDo4M/pXGafXHUwMDAzzPTAYyEmQHkxNFx1MDAxN072bjVccu6UcSRLT15cdTAwMWbqe5Y9XCJfh3jVyTYnsmZcdTAwMWFXYuvuzFn+qIFyUYNT/lx1MDAwN4XHQ0RcdTAwMTM4KyrhMMXS5SSUXZDbvJ/UnGJV0YRpVCxbTzFcdTAwMGauyzFcdTAwMDHTLlx1MDAxONOV5Th4JFx1MDAwNEJKsZZMwuJs2XLz9mYlRZrbdPFcdTAwMDVuXHUwMDBieMpcdTAwMDCBqyuSPbeH5oCEn9jhi1x1MDAxM3N0XHUwMDE2nqG389zWN1Mk7oHOglx1MDAwMHFcIjlUV+S+lytcdTAwMTRcdTAwMTfYg6BWgFx1MDAxYlx1MDAwNWfmXHUwMDA0u1AoXHUwMDAxVVx1MDAxMLxcZlVcdTAwMWO8XHUwMDE3zF2/QuGST3GS5LrvsVx1MDAwNFx0XHUwMDAyXaBcdTAwMDJ3+OhcbpT/kiFcdFbuXu/XXHUwMDFilaxhpTeqNjH5xvxpoihpnCZpXHUwMDE0/DSz8Sn9RvU1XHUwMDFhn0o+NzTbXHUwMDE407TpXHUwMDBmXHUwMDA2R1x1MDAxNnZrYsPgXHUwMDFjwmB8y27V5kVoLndmYyCHwcaYujlHTOGArzeu/1x1MDAwN0PH3J0ifQ== Base screen(partial visible)Top-most screenbackground: rgba(0,0,255,0.5);Hello World!

    Note

    Although parts of other screens may be made visible with background alpha, only the top-most is active (can respond to mouse and keyboard).

    One use of background alpha is to style modal dialogs (see below).

    "},{"location":"guide/screens/#modal-screens","title":"Modal screens","text":"

    Screens may be used to create modal dialogs, where the main interface is temporarily disabled (but still visible) while the user is entering information.

    The following example pushes a screen when you hit the Q key to ask you if you really want to quit. From the quit screen you can click either Quit to exit the app immediately, or Cancel to dismiss the screen and return to the main screen.

    OutputOutput (after pressing Q)modal01.pymodal01.tcss

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    ModalApp \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2588 \u2588\u2588 \u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588 \u2588QuitCancel\u2588 \u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588

    modal01.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(Screen):\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.app.exit()\n        else:\n            self.app.pop_screen()\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        self.push_screen(QuitScreen())\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    Note the request_quit action in the app which pushes a new instance of QuitScreen. This makes the quit screen active. If you click Cancel, the quit screen calls pop_screen to return the default screen. This also removes and deletes the QuitScreen object.

    There are two flaws with this modal screen, which we can fix in the same way.

    The first flaw is that the app adds a new quit screen every time you press Q, even when the quit screen is still visible. Consequently if you press Q three times, you will have to click Cancel three times to get back to the main screen. This is because bindings defined on App are always checked, and we call push_screen for every press of Q.

    The second flaw is that the modal dialog doesn't look modal. There is no indication that the main interface is still there, waiting to become active again.

    We can solve both those issues by replacing our use of Screen with ModalScreen. This screen sub-class will prevent key bindings on the app from being processed. It also sets a background with a little alpha to allow the previous screen to show through.

    Let's see what happens when we use ModalScreen.

    OutputOutput (after pressing Q)modal02.pymodal01.tcss

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0i\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0\u2588\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u2588 Fear\u00a0is\u00a0th\u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 I\u00a0will\u00a0fac\u2588\u2588 I\u00a0will\u00a0per\u2588\u2588\u2585\u2585 And\u00a0when\u00a0i\u2588\u2588 Where\u00a0the\u00a0\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588QuitCancel\u2588 Fear\u00a0is\u00a0th\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0fac\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

    modal02.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(ModalScreen):\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.app.exit()\n        else:\n            self.app.pop_screen()\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        \"\"\"Action to display the quit dialog.\"\"\"\n        self.push_screen(QuitScreen())\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    Now when we press Q, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input.

    "},{"location":"guide/screens/#returning-data-from-screens","title":"Returning data from screens","text":"

    It is a common requirement for screens to be able to return data. For instance, you may want a screen to show a dialog and have the result of that dialog processed after the screen has been popped.

    To return data from a screen, call dismiss() on the screen with the data you wish to return. This will pop the screen and invoke a callback set when the screen was pushed (with push_screen).

    Let's modify the previous example to use dismiss rather than an explicit pop_screen.

    modal03.pymodal01.tcss modal03.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass QuitScreen(ModalScreen[bool]):  # (1)!\n    \"\"\"Screen with a dialog to quit.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Grid(\n            Label(\"Are you sure you want to quit?\", id=\"question\"),\n            Button(\"Quit\", variant=\"error\", id=\"quit\"),\n            Button(\"Cancel\", variant=\"primary\", id=\"cancel\"),\n            id=\"dialog\",\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        if event.button.id == \"quit\":\n            self.dismiss(True)\n        else:\n            self.dismiss(False)\n\n\nclass ModalApp(App):\n    \"\"\"An app with a modal dialog.\"\"\"\n\n    CSS_PATH = \"modal01.tcss\"\n    BINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Label(TEXT * 8)\n        yield Footer()\n\n    def action_request_quit(self) -> None:\n        \"\"\"Action to display the quit dialog.\"\"\"\n\n        def check_quit(quit: bool) -> None:\n            \"\"\"Called when QuitScreen is dismissed.\"\"\"\n            if quit:\n                self.exit()\n\n        self.push_screen(QuitScreen(), check_quit)\n\n\nif __name__ == \"__main__\":\n    app = ModalApp()\n    app.run()\n
    1. See below for an explanation of the [bool]
    modal01.tcss
    QuitScreen {\n    align: center middle;\n}\n\n#dialog {\n    grid-size: 2;\n    grid-gutter: 1 2;\n    grid-rows: 1fr 3;\n    padding: 0 1;\n    width: 60;\n    height: 11;\n    border: thick $background 80%;\n    background: $surface;\n}\n\n#question {\n    column-span: 2;\n    height: 1fr;\n    width: 1fr;\n    content-align: center middle;\n}\n\nButton {\n    width: 100%;\n}\n

    In the on_button_pressed message handler we call dismiss with a boolean that indicates if the user has chosen to quit the app. This boolean is passed to the check_quit function we provided when QuitScreen was pushed.

    Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. This makes it easier for the app to perform any cleanup actions prior to exiting, for example.

    Returning data in this way can help keep your code manageable by making it easy to re-use your Screen classes in other contexts.

    "},{"location":"guide/screens/#typing-screen-results","title":"Typing screen results","text":"

    You may have noticed in the previous example that we changed the base class to ModalScreen[bool]. The addition of [bool] adds typing information that tells the type checker to expect a boolean in the call to dismiss, and that any callback set in push_screen should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs.

    "},{"location":"guide/screens/#waiting-for-screens","title":"Waiting for screens","text":"

    It is also possible to wait on a screen to be dismissed, which can feel like a more natural way of expressing logic than a callback. The push_screen_wait() method will push a screen and wait for its result (the value from Screen.dismiss()).

    This can only be done from a worker, so that waiting for the screen doesn't prevent your app from updating.

    Let's look at an example that uses push_screen_wait to ask a question and waits for the user to reply by clicking a button.

    questions01.pyquestions01.tcssOutput questions01.py
    from textual import on, work\nfrom textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Label\n\n\nclass QuestionScreen(Screen[bool]):\n    \"\"\"Screen with a parameter.\"\"\"\n\n    def __init__(self, question: str) -> None:\n        self.question = question\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.question)\n        yield Button(\"Yes\", id=\"yes\", variant=\"success\")\n        yield Button(\"No\", id=\"no\")\n\n    @on(Button.Pressed, \"#yes\")\n    def handle_yes(self) -> None:\n        self.dismiss(True)  # (1)!\n\n    @on(Button.Pressed, \"#no\")\n    def handle_no(self) -> None:\n        self.dismiss(False)  # (2)!\n\n\nclass QuestionsApp(App):\n    \"\"\"Demonstrates wait_for_dismiss\"\"\"\n\n    CSS_PATH = \"questions01.tcss\"\n\n    @work  # (3)!\n    async def on_mount(self) -> None:\n        if await self.push_screen_wait(  # (4)!\n            QuestionScreen(\"Do you like Textual?\"),\n        ):\n            self.notify(\"Good answer!\")\n        else:\n            self.notify(\":-(\", severity=\"error\")\n\n\nif __name__ == \"__main__\":\n    app = QuestionsApp()\n    app.run()\n
    1. Dismiss with True when pressing the Yes button.
    2. Dismiss with False when pressing the No button.
    3. The work decorator will make this method run in a worker (background task).
    4. Will return a result when the user clicks one of the buttons.
    questions01.tcss
    QuestionScreen {\n    layout: grid;\n    grid-size: 2 2;                \n    align: center bottom;\n}\n\nQuestionScreen > Label {\n    margin: 1;       \n    text-align: center;\n    column-span: 2;    \n    width: 1fr;\n}\n\nQuestionScreen Button {\n    margin: 2; \n    width: 1fr;     \n}\n

    QuestionsApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Do\u00a0you\u00a0like\u00a0Textual?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 YesNo \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    The mount handler on the app is decorated with @work, which makes the code run in a worker (background task). In the mount handler we push the screen with the push_screen_wait. When the user presses one of the buttons, the screen calls dismiss() with either True or False. This value is then returned from the push_screen_wait method in the mount handler.

    "},{"location":"guide/screens/#modes","title":"Modes","text":"

    Some apps may benefit from having multiple screen stacks, rather than just one. Consider an app with a dashboard screen, a settings screen, and a help screen. These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. But we may still want each individual screen to have a navigation stack where we can push and pop screens.

    In Textual we can manage this with modes. A mode is simply a named screen stack, which we can switch between as required. When we switch modes, the topmost screen in the new mode becomes the active visible screen.

    The following diagram illustrates such an app with modes. On startup the app switches to the \"dashboard\" mode which makes the top of the stack visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 \"dashboard\"\"help\"\"settings\"Active (visible)

    If we later change the mode to \"settings\", the top of that mode's screen stack becomes visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== \"dashboard\"\"help\"\"settings\"Active

    To add modes to your app, define a MODES class variable in your App class which should be a dict that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. However you specify it, the values in MODES set the base screen for each mode's screen stack.

    You can switch between these screens at any time by calling App.switch_mode. When you switch to a new mode, the topmost screen in the new stack becomes visible. Any calls to App.push_screen or App.pop_screen will affect only the active mode.

    Let's look at an example with modes:

    modes01.pyOutputOutput (after pressing S)
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Footer, Placeholder\n\n\nclass DashboardScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Dashboard Screen\")\n        yield Footer()\n\n\nclass SettingsScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Settings Screen\")\n        yield Footer()\n\n\nclass HelpScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Placeholder(\"Help Screen\")\n        yield Footer()\n\n\nclass ModesApp(App):\n    BINDINGS = [\n        (\"d\", \"switch_mode('dashboard')\", \"Dashboard\"),  # (1)!\n        (\"s\", \"switch_mode('settings')\", \"Settings\"),\n        (\"h\", \"switch_mode('help')\", \"Help\"),\n    ]\n    MODES = {\n        \"dashboard\": DashboardScreen,  # (2)!\n        \"settings\": SettingsScreen,\n        \"help\": HelpScreen,\n    }\n\n    def on_mount(self) -> None:\n        self.switch_mode(\"dashboard\")  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = ModesApp()\n    app.run()\n
    1. switch_mode is a builtin action to switch modes.
    2. Associates DashboardScreen with the name \"dashboard\".
    3. Switches to the dashboard mode.

    ModesApp Dashboard\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

    ModesApp Settings\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

    Here we have defined three screens. One for a dashboard, one for settings, and one for help. We've bound keys to each of these screens, so the user can switch between the screens.

    Pressing D, S, or H switches between these modes.

    "},{"location":"guide/styles/","title":"Styles","text":"

    In this chapter we will explore how you can apply styles to your application to create beautiful user interfaces.

    "},{"location":"guide/styles/#styles-object","title":"Styles object","text":"

    Every Textual widget class provides a styles object which contains a number of attributes. These attributes tell Textual how the widget should be displayed. Setting any of these attributes will update the screen accordingly.

    Note

    These docs use the term screen to describe the contents of the terminal, which will typically be a window on your desktop.

    Let's look at a simple example which sets styles on screen (a special widget that represents the screen).

    screen.py
    from textual.app import App\n\n\nclass ScreenApp(App):\n    def on_mount(self) -> None:\n        self.screen.styles.background = \"darkblue\"\n        self.screen.styles.border = (\"heavy\", \"white\")\n\n\nif __name__ == \"__main__\":\n    app = ScreenApp()\n    app.run()\n

    The first line sets the background style to \"darkblue\" which will change the background color to dark blue. There are a few other ways of setting color which we will explore later.

    The second line sets border to a tuple of (\"heavy\", \"white\") which tells Textual to draw a white border with a style of \"heavy\". Running this code will show the following:

    ScreenApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    "},{"location":"guide/styles/#styling-widgets","title":"Styling widgets","text":"

    Setting styles on screen is useful, but to create most user interfaces we will also need to apply styles to other widgets.

    The following example adds a static widget which we will apply some styles to:

    widget.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass WidgetApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(\"Textual\")\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.border = (\"heavy\", \"white\")\n\n\nif __name__ == \"__main__\":\n    app = WidgetApp()\n    app.run()\n

    The compose method stores a reference to the widget before yielding it. In the mount handler we use that reference to set the same styles on the widget as we did for the screen example. Here is the result:

    WidgetApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Textual\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Widgets will occupy the full width of their container and as many lines as required to fit in the vertical direction.

    Note how the combined height of the widget is three rows in the terminal. This is because a border adds two rows (and two columns). If you were to remove the line that sets the border style, the widget would occupy a single row.

    Information

    Widgets will wrap text by default. If you were to replace \"Textual\" with a long paragraph of text, the widget will expand downwards to fit.

    "},{"location":"guide/styles/#colors","title":"Colors","text":"

    There are a number of style attributes which accept colors. The most commonly used are color which sets the default color of text on a widget, and background which sets the background color (beneath the text).

    You can set a color value to one of a number of pre-defined color constants, such as \"crimson\", \"lime\", and \"palegreen\". You can find a full list in the Color API.

    Here's how you would set the screen background to lime:

    self.screen.styles.background = \"lime\"\n

    In addition to color names, you can also use any of the following ways of expressing a color:

    • RGB hex colors starts with a # followed by three pairs of one or two hex digits; one for the red, green, and blue color components. For example, #f00 is an intense red color, and #9932CC is dark orchid.
    • RGB decimal color start with rgb followed by a tuple of three numbers in the range 0 to 255. For example rgb(255,0,0) is intense red, and rgb(153,50,204) is dark orchid.
    • HSL colors start with hsl followed by a angle between 0 and 360 and two percentage values, representing Hue, Saturation and Lightness. For example hsl(0,100%,50%) is intense red and hsl(280,60%,49%) is dark orchid.

    The background and color styles also accept a Color object which can be used to create colors dynamically.

    The following example adds three widgets and sets their color styles.

    colors01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Static\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(\"Textual One\")\n        yield self.widget1\n        self.widget2 = Static(\"Textual Two\")\n        yield self.widget2\n        self.widget3 = Static(\"Textual Three\")\n        yield self.widget3\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"#9932CC\"\n        self.widget2.styles.background = \"hsl(150,42.9%,49.4%)\"\n        self.widget2.styles.color = \"blue\"\n        self.widget3.styles.background = Color(191, 78, 96)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    Here is the output:

    ColorApp Textual\u00a0One Textual\u00a0Two Textual\u00a0Three

    "},{"location":"guide/styles/#alpha","title":"Alpha","text":"

    Textual represents color internally as a tuple of three values for the red, green, and blue components.

    Textual supports a common fourth value called alpha which can make a color translucent. If you set alpha on a background color, Textual will blend the background with the color beneath it. If you set alpha on the text color, then Textual will blend the text with the background color.

    There are a few ways you can set alpha on a color in Textual.

    • You can set the alpha value of a color by adding a fourth digit or pair of digits to a hex color. The extra digits form an alpha component which ranges from 0 for completely transparent to 255 (completely opaque). Any value between 0 and 255 will be translucent. For example \"#9932CC7f\" is a dark orchid which is roughly 50% translucent.
    • You can also set alpha with the rgba format, which is identical to rgb with the additional of a fourth value that should be between 0 and 1, where 0 is invisible and 1 is opaque. For example \"rgba(192,78,96,0.5)\".
    • You can add the a parameter on a Color object. For example Color(192, 78, 96, a=0.5) creates a translucent dark orchid.

    The following example shows what happens when you set alpha on background colors:

    colors01.py
    from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Static\n\n\nclass ColorApp(App):\n    def compose(self) -> ComposeResult:\n        self.widgets = [Static(\"\") for n in range(10)]\n        yield from self.widgets\n\n    def on_mount(self) -> None:\n        for index, widget in enumerate(self.widgets, 1):\n            alpha = index * 0.1\n            widget.update(f\"alpha={alpha:.1f}\")\n            widget.styles.background = Color(191, 78, 96, a=alpha)\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n

    Notice that at an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.

    ColorApp alpha=0.1 alpha=0.2 alpha=0.3 alpha=0.4 alpha=0.5 alpha=0.6 alpha=0.7 alpha=0.8 alpha=0.9 alpha=1.0

    "},{"location":"guide/styles/#dimensions","title":"Dimensions","text":"

    Widgets occupy a rectangular region of the screen, which may be as small as a single character or as large as the screen (potentially larger if scrolling is enabled).

    "},{"location":"guide/styles/#box-model","title":"Box Model","text":"

    The following styles influence the dimensions of a widget.

    • width and height define the size of the widget.
    • padding adds optional space around the content area.
    • border draws an optional rectangular border around the padding and the content area.

    Additionally, the margin style adds space around a widget's border, which isn't technically part of the widget, but provides visual separation between widgets.

    Together these styles compose the widget's box model. The following diagram shows how these settings are combined:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT2txcdTAwMTb+3l/h+H4tu/t+6cyZM9VqrbfWevfMO51cYlx1MDAwMaJAaFx1MDAxMlx1MDAxNHyn//2sXHUwMDFklCSERFCw6Dn50EpcdTAwMTKSxd7redaz1r78825lZTVcdTAwMWF03dWPK6tuv+q0vFrg3K6+t+dv3CD0/Fx1MDAwZVxcovHn0O9cdTAwMDXV+M5mXHUwMDE0dcOPXHUwMDFmPrSd4NqNui2n6qJcdTAwMWIv7DmtMOrVPFx1MDAxZlX99lx1MDAwNy9y2+G/7b/7Ttv9V9dv16JcdTAwMDAlL6m4NS/yg+G73JbbdjtRXGJP/1x1MDAwZnxeWfkn/jdlXeBWI6fTaLnxXHUwMDE34kuJgZzT8bP7fic2llBFleCck9FcdTAwMWRe+Fx1MDAxOd5cdTAwMTe5NbhcXFx1MDAwN5vd5Io9tTo46Tt6Y//TbvubXHUwMDFmnISHXHUwMDFkv16tJa+te63WYTRoXHKbwqk2e0HKqDBcbvxr99SrRU379rHzo+/V/MhcdTAwMWEwulx1MDAxY/i9RrPjhmHmS37XqXrRwJ7DeHR22FxmXHUwMDFmV5IzfdtcdTAwMDZYXCIhpMGYXHUwMDEwybA0cnQ5flx1MDAwMMdIY2FcYiZGKEFcdTAwMTVcdTAwMWYzbd1vQW+AaX/h+Ehsu3Sq11xyMLBTXHUwMDFi3Vx1MDAxM1x1MDAwNU4n7DpcdTAwMDH0WXLf7f2PXHUwMDE22CCmNJVUXHUwMDBiplxyS35P0/VcdTAwMWHNyFx1MDAxYUtcdMJaXHUwMDE4psTwbYmxoVx1MDAxYndcZtjJXGI8JfmR1oTu11rsI3+Pt2vTXHS69823XHUwMDFh2lx1MDAwZinzreVcdTAwMWLjXHUwMDBllnayVN9/r+v+vt5vXa5HV0dH52dVXHUwMDE11ndGz8p4pFx1MDAxM1x1MDAwNP7t6ujK7/u/XHUwMDEy03rdmjP0MlwiJSOSSo0l06PrLa9zXHJcdTAwMTc7vVYrOedXr1x1MDAxM8eMz/5+/1x1MDAwNERIZVxuXHUwMDExYYji4Fx1MDAwNFRNjYjdra+tur9f3aq3XHUwMDA3P1x1MDAxYd7N7lx1MDAwMd1cZlx1MDAwYlx1MDAxMFx1MDAxMfqA75nxMPatx+DAXHUwMDFlRYNcdTAwMDI0XHUwMDE4TVx1MDAxOVx1MDAxNVx1MDAxYyshWFx1MDAxNlxylGokJSZKXHS4R6cvj6NB1FmtykvR8Fx1MDAxN69Kty7ySGBCIS1cdTAwMDSXWok8XGKoMIhcdTAwMWFcdTAwMDNewVx1MDAxOcNU5EFAXHUwMDE5XHUwMDEzklxirl9cdTAwMTZcdTAwMDTVb7vnzbOt49rAmE5vn3RVs+a9Qlx1MDAxMHBZXGZcdTAwMDImMeVUSjE1XGKud6+u2mHw6zRw+4HZONu46PHPT1x1MDAwYlx1MDAwYrRcYlx1MDAwNjUnbM43LFxiRpFSXFxIYaiSWpAsXHUwMDBlXHUwMDE0uKDWWlx1MDAxOUOoIIqwQlx1MDAxY7hSqedEXHUwMDA18O88XHUwMDA0SMq1XHUwMDFmiJ9pXGJh0DX8pZz+wZdcIrdcdTAwMWZlvXzY8TtcdTAwMTef9k5+bW1drG1dXHUwMDA0P2818/prTsrn309+7PDLd1x1MDAwM7355Yiz3Z1+c933mt+rXHUwMDFi11+WXHUwMDEzS5nfn5Z/KZCMwchIQjhcdTAwMDeKmlx1MDAxYUU3XHUwMDFlZnzT7Kvewefq9aU52D6W3+YsrmZcZiaPg0hcdTAwMWGDuLA/lEpmtKFcdTAwMTlcdTAwMTBcdMKQ1pJcdTAwMTgjtNRcdTAwMTBNXHUwMDE2pqwkzUOIilx1MDAxY4KwkFQxiHHzR9A8nTHpdL9cdTAwMTNcdTAwMWR6d7bdKc6c3XTaXmuQ6bfYS8HSPSdoeJ10W4YuvDMmd525+1PLa1g/Xm259ayDR1x1MDAxZWQjo8uRn/rlVXi7XHUwMDAzj1x1MDAwYr7Wxn+FXHUwMDFmePBmp3WUteRJ2GKiXHUwMDEwW1x1MDAwNGSaUcDabGpwXHLCzs7hQe1qw2l+3dq9lp2Dm7D5gplcdTAwMGJ+XCK6IERRK4K0XHLLXFxk0Fx1MDAwNalcdTAwMWJcdTAwMDLgYSpccmRcdTAwMTOMXHUwMDFhtTB4pXKicngpSFxcXGZ+YVXmqeb2TXV7/etGh31yu+etSPw8mmskSdTSosH73anVvE5jXHUwMDE50PtgytNCI1bjZ1x1MDAxZuBcdTAwMGLaXHUwMDFkXHUwMDEyXFwpplx1MDAwZo2Tdcayo1cwVVwiMFx1MDAxOSdIvZDA5Fx1MDAxM1x1MDAwNCZNvW9cYl9gXHUwMDFiyLYgUL98cFxcXHUwMDE0vlhcdTAwMGVf63BcdTAwMTmsWoGmciaDzExcdTAwMDZZXHUwMDE1vuVcdTAwMDYlMGt7tVo628pcIu2xJGlcdTAwMWN8XHUwMDE5O0tcdTAwMTFYnuhpXlx1MDAwNEPCpWCCSjJ9tWPnR6O5dbm5b1x1MDAwNs2rk8b+USDO+rdcdTAwMDU4rFx1MDAwNn5cdTAwMThWmk5UbVx1MDAxNmFxvNC2OJlcdTAwMWFcdTAwMTc9tFx1MDAxMYRgI1xyIYxnsEipRJBZXHRNjZREM1lcXFx1MDAwMpyi6FGKxcdcdTAwMGJcdTAwMWaGcCzzwVVcYlxyvVx1MDAwNVx1MDAwMvZlY+vF55/O2t2nauv48MugvXf16cfBXHUwMDE1ny62lmZ/e9v9XHLj17c3RXBUOe+37rw9df3HYnYpwIbvn1x1MDAwNC4qcVx1MDAxMbooMUopkGVTg6u8pWdcdTAwMDZXYSVl7uBcdTAwMTJGIU4heGCNtVx1MDAwMcfO5oBcdTAwMTSucmhcdGIgXHUwMDAzNGRxOSAjiHJcYqZcdTAwMWNiKSeQdubxxTRcdTAwMTJcdTAwMDRMZFx1MDAxNFBuMOXjKCNYWv+RqXxyapjFpr50XHUwMDEwXGYjJ4jWvE6s1D6mkPYwcjSMPj0xOOmJdXzt4cPTtmxf3ahcdTAwMTM/XHUwMDA1N1xim9Ve7Fx1MDAwMlxiY26w4Fx1MDAxYfqCXHUwMDFhbFL3NJxu3ESIKMNcdTAwMTVcdTAwMTAp3Mah3+/vXHUwMDE4XHUwMDAxftXt1Fx1MDAxZTepPJikTKpgRDU3jFxi8DBwMS1VziiKjFx1MDAwMXMgXHSC+5SUQuWMajlhtO63257Ved99r1x1MDAxM403cdyWnyzam66Tk8fwo9LXxmmha5+YpdPkr5VcdTAwMDQy8YfR33+/n3h3oSvbo5Lz4uRx79L/z6rZ7buK+Ixhrlx1MDAxOZMzpNzlLvdcInz2RN1O49ZXoFx1MDAxNojmNFW5XHUwMDE51rQ4XHUwMDAyNU9cdTAwMTlIXHUwMDA1cDSlx+yaY00rXHT1JUm35sImXHUwMDEwL1dcdTAwMTR+ti5YQPyeJSfI59xrflBLS/s/l3LfW/I0OWL5sVxivuCwilE9vdQv12fzXHUwMDE505k7cpUmXGJcdTAwMTRcYoGMmyvKx1Q+5DqIgFx1MDAxMiFcdTAwMDRcdTAwMTBDXHUwMDE0Xdw4P1x1MDAwNCwhpVxyVMDdXFzTXHTFaWB4XHUwMDA29zCOXHUwMDE52GnrezkpXCKYYoxi/lx1MDAwNGQvg1x1MDAxNFx1MDAxOVx1MDAwZp5zUFx1MDAwNMNYr5GWXHUwMDE0Q8tcdTAwMTgsQX/wtERJaVx1MDAwNoWNVkpyyZTQkHK9akFQ6FH2qOSdaUZFUMwpjJeMXHUwMDE0K2MoVnz6Ql75JJIlZlx1MDAxNWh6XHUwMDAzv5RA2/Ox/IZhXHUwMDA0XHQ7tFx1MDAxM8EgkPi4XfNkXHUwMDE1XHTv4LFcdTAwMTZcdTAwMDbP5olcZs7QitJcdTAwMTRslWCtJFxc5jNcdTAwMWNcdTAwMDHGgqh5ylx1MDAxONjrpJXyWWsp0oCO5EZcdTAwMDCSKFFcdTAwMDI4I2m8JPl55SxS5EH2yPvOjCxcdTAwMTJrplx0JKJTXHUwMDEyepxDpGBcdTAwMDY4LTUp6zFcdTAwMGVZ29w4ucWuUP0tsbl95ZxXfTlY9nFyYFxyXHUwMDA09IHt1Cqp+Vx1MDAxOIdAXHUwMDFlh1xmqFx1MDAxMiPhXHUwMDA2I1x1MDAwNF/kXHUwMDE0RESkMpPLj1x1MDAwNI1XJlx1MDAxZkhDXHUwMDEzhTGHZPOtkUb2YfPFcubaXFyBPKFcdTAwMTftMeq/OVx1MDAwMVfIYuBqJY2tqE9fXHUwMDEwYNu/blx1MDAwZqtcdTAwMDbj3unNSbOmyMHaN7b0wIWmVpBdXHUwMDFiqZSwXHUwMDAzXGJcdTAwMTngcq3tbF3OoNlcdTAwMDEgIDdcdTAwMTdcdTAwMDZcXFx1MDAwZbJXXHUwMDEwkVx1MDAxZVx1MDAxOVx1MDAxOOFcdTAwMTYjXHUwMDE2K8NcdMDlXHUwMDAwXFyRKVP8XHUwMDFmuH9cdTAwMTC4+V60RyXpwHlcdPd0oT1cdTAwMDddyCVcdTAwMTQ1eHrofu7XpXvxq7Le9DaOf7Y/fz6+/LGx9NBVXHUwMDE0KfBHyqmiXHUwMDEyXHUwMDA0XVx1MDAwNrqMcWQ4t9OIXHUwMDE1aFx1MDAxZbLIaoBRRnHGXGJjoKxcZp9Q14PcXHUwMDAxXHSMNYeeseV3kUoj7mc8cylcdTAwMTSh4pVcdTAwMDK5SJz7Z8e7J25w1T350Vx1MDAxZmzc7eytNUNZMFxugDlcdTAwMTaMQdBRiimuVaoqnoxNUFx1MDAwMklcdTAwMWFcdTAwMTNwp5mQ879cdTAwMTiFLFTHV4pdKr6c96Z50VxuKFg2fvqBV5TClNmumZpWSO07ub08qG5+ObxsrK/fnlx1MDAwN+J4b/lphSBbeLKDUYaQ8Wk9SiNlsJ3xg+2s2MVccndcdTAwMTJkbbAlXHUwMDA3zOFl6XZcdTAwMWbRiiHgKKBbiKCaKZErXHUwMDA2KEq4XHUwMDA2J3mlsv7ZpFx1MDAwMjKaQKbLJFx1MDAwNVxmYYEpyXOKRra4Q5WhmFx1MDAxMSHMXHUwMDFi5ZRid7LHuCPNyCdFI466eFx1MDAwMoWCfrEj0dOLlPJeX1Y24Vx1MDAxY0lNtDBKXGLI/rOFXHUwMDAxxlx1MDAwNbI+Z9dCcWpSq3bmXlx1MDAxN0hcdTAwMWVdMthoQDpcdTAwMTKhzVx1MDAwYk/wLY9cdTAwMTNcdTAwMTlPm2lcdTAwMTJSuawtfe6D2y+C5Z40iLk17LWUXHUwMDAz/KlBzHtLSlx0oajiYEwhIVx1MDAxMMUhrIKomX698vlp4/i6urfryruoe0hMv1x1MDAxMX3dWnZG4DZtXHUwMDExTFPQd5ZcdTAwMDGzaVx1MDAwYsVcdTAwMDYkrq38gPbA6ULiXHUwMDAyhlx1MDAxYqC9gfCp4cD8lEzQXHUwMDE3QE9cdTAwMTiDXHUwMDEw1Vx1MDAwMi5TjXW+XHUwMDAwXHUwMDAxeZfAdsnz65RcdTAwMThvrVx1MDAwMFHcq/ao5Dt0xkhfXHUwMDA07LQ8XHUwMDFkn5sgjLaZw/TDiL1m55Z4XHUwMDBl+bV5Snb5trk46vhF85CXXHUwMDA215pcdESltPPqMNCcyuZcclx1MDAxNEtEQIcyw5nGgJrFVVx1MDAxMjHSRmktXHUwMDA1SCwstZ5Uj+B2XHUwMDE3XHUwMDAyUIFK2GXimuSHXHUwMDExJaRcdTAwMGWcavXmZie8VlxcXHUwMDE3dao9Krn+nFx1MDAxMdbFXHUwMDA1gZJcdTAwMTVcdTAwMDbUzlx1MDAxMOCQXHUwMDExT4/s5vb15UG3Ujlccu+2XHUwMDBl1ytsoPYrRZOgl1x1MDAwNtmSaGR3XHUwMDE2XHUwMDAxXHUwMDE1z5igKrtMj1x1MDAxOMu5XHUwMDE4Wt72S3pa8twrXHUwMDAyTFwiXHUwMDEwXGbA21x1MDAwNOjFpEZdUyN8QENcdTAwMWOua8ZcdTAwMTRcdTAwMDVcdJFbZc4451pcdTAwMTP6SuN1UUngSNd37i62XHUwMDFiP9yzjbWdwDn9eXjzs6DOSOxcXFx1MDAwZYhJXHUwMDEyMmHDSXqyTVJntHOMsVx1MDAxMZgzm1x1MDAxMt3f8NaKXHUwMDAylUKXXHUwMDFhXs1509xoJb1mLbdxXHUwMDExwVSotPZ9jFakS25PXHUwMDBm1k7Xe7+O62FwuH6MXbr0tGIoMsDXkFx1MDAwYmBlXGZcdTAwMWKjXHUwMDE1bZBcItb9qFLMyOLlg8+mXHUwMDE1amfcXHUwMDAxfeF4jDNN+JlcdTAwMTFIwlx0RJ94sJHp9GLke8VAiMBcdTAwMDJj/r/KLFx1MDAxOMHPl1RBXG5cdTAwMGKQsqFxwvxcIoKInZwllMKEQ7fn1zG8XHJmKXYqe1Qm+NOM1FJUclQlK1wiXHK1vjLDfMbyvl9WXrFNXHUwMDBmylx1MDAxZNI7q81otuRIlERSc6OJsfNcdTAwMWQoW1x1MDAxY7GI5NFlu1xuQMakiZb6XHS88ZyiY7lcdTAwMTTN+NpMRcfyWFT63Fx1MDAwN8dfXHUwMDA02z2p6Dh03pRcdTAwMDP8qZrj0JCnaVxyXjxVgjOioEVnmJ1YvqvR0s5wZlxiYo3dQ4RplopLw0lOQiFiXGaVIDMoXHUwMDExophcdTAwMTBcdTAwMThnXHUwMDBlf1ZlXHUwMDAyXHUwMDEzO81cdTAwMTAzXGJcdTAwMGbAw8JM2nfEqlwiXHUwMDAxYcFcdTAwMTZBIUayXFxcbmOkgUeItzdZsUiClO8tMFx1MDAxMlx1MDAxN1x1MDAxMlx1MDAxOWm1uuTSjk7zlJhP0lx1MDAxZrtcdTAwMDCUXHUwMDAw30L+w6lcdTAwMDHflzlcdTAwMDXymnRGpcSn4ut5d5pRaFx1MDAxNOcwqmRcdTAwMDcjIZmgRE5PLOWb3CwtsVDEscF2koFcdTAwMTFybPKkXHUwMDA0satBXGZcdTAwMTOFXHUwMDAxPCUrKZ/PK5rH0/ol1liI9JKqpDKikVx1MDAwNPlHIKM1oI7YhFx1MDAwNVlgpmBY0SdswbBcZrxSuD6idG+sLDdASlwiuYYkRoNOJGpCZUTYtTKScFx1MDAwNUhcdTAwMTOg3lx1MDAxZiqNby2BqVx1MDAxNDuVPfLuNCOtlG7rYkjhts5cdTAwMTRDjMaE0umzmIprjs+jy7NQRc2Ts1x1MDAwZebru79cdTAwMGVcbqhlubZ10Vx1MDAxONJlu1kpt9BMVyaG27pcdTAwMThky3eGXHRNKFBNMcM8f1tcdTAwMTeCXHUwMDE4J0VcdTAwMGIrqKCIMUj9qWRcdTAwMDVcdTAwMTO1IeBcdTAwMWFcdTAwMGXukrTEsq/jLs1yXHUwMDE2ur+LgoQwtfXmtPu7vLt/6KrT7Vx1MDAxZUbwyFx1MDAxMSlCW3u1++wneczqjeferk3Y1bhcdTAwMWVcdTAwMWbW5LhcdTAwMTEsQlxc29L//H73+7/nXHUwMDBiXHUwMDAzXCIifQ== MarginPaddingContent areaBorderHeightWidth"},{"location":"guide/styles/#width-and-height","title":"Width and height","text":"

    Setting the width restricts the number of columns used by a widget, and setting the height restricts the number of rows. Let's look at an example which sets both dimensions.

    dimensions01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.height = 10\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    This code produces the following result.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path.

    Note how the text wraps in the widget, and is cropped because it doesn't fit in the space provided.

    "},{"location":"guide/styles/#auto-dimensions","title":"Auto dimensions","text":"

    In practice, we generally want the size of a widget to adapt to its content, which we can do by setting a dimension to \"auto\".

    Let's set the height to auto and see what happens.

    dimensions02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.height = \"auto\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    If you run this you will see the height of the widget now grows to accommodate the full text:

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    "},{"location":"guide/styles/#units","title":"Units","text":"

    Textual offers a few different units which allow you to specify dimensions relative to the screen or container. Relative units can better make use of available space if the user resizes the terminal.

    • Percentage units are given as a number followed by a percent (%) symbol and will set a dimension to a proportion of the widget's parent size. For instance, setting width to \"50%\" will cause a widget to be half the width of its parent.
    • View units are similar to percentage units, but explicitly reference a dimension. The vw unit sets a dimension to a percentage of the terminal width, and vh sets a dimension to a percentage of the terminal height.
    • The w unit sets a dimension to a percentage of the available width (which may be smaller than the terminal size if the widget is within another widget).
    • The h unit sets a dimension to a percentage of the available height.

    The following example demonstrates applying percentage units:

    dimensions03.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.height = \"80%\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    With the width set to \"50%\" and the height set to \"80%\", the widget will keep those relative dimensions when resizing the terminal window:

    60 x 2080 x 30120 x 40

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0 total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0 the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0 its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    "},{"location":"guide/styles/#fr-units","title":"FR units","text":"

    Percentage units can be problematic for some relative values. For instance, if we want to divide the screen into thirds, we would have to set a dimension to 33.3333333333% which is awkward. Textual supports fr units which are often better than percentage-based units for these situations.

    When specifying fr units for a given dimension, Textual will divide the available space by the sum of the fr units on that dimension. That space will then be divided amongst the widgets as a proportion of their individual fr values.

    Let's look at an example. We will create two widgets, one with a height of \"2fr\" and one with a height of \"1fr\".

    dimensions04.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass DimensionsApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.height = \"2fr\"\n        self.widget2.styles.height = \"1fr\"\n\n\nif __name__ == \"__main__\":\n    app = DimensionsApp()\n    app.run()\n

    The total fr units for height is 3. The first widget will have a screen height of two thirds because its height style is set to 2fr. The second widget's height style is 1fr so its screen height will be one third. Here's what that looks like.

    DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    "},{"location":"guide/styles/#maximum-and-minimums","title":"Maximum and minimums","text":"

    The same units may also be used to set limits on a dimension. The following styles set minimum and maximum sizes and can accept any of the values used in width and height.

    • min-width sets a minimum width.
    • max-width sets a maximum width.
    • min-height sets a minimum height.
    • max-height sets a maximum height.
    "},{"location":"guide/styles/#padding","title":"Padding","text":"

    Padding adds space around your content which can aid readability. Setting padding to an integer will add that number additional rows and columns around the content area. The following example sets padding to 2:

    padding01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.padding = 2\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n

    Notice the additional space around the text:

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past, I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0 Only\u00a0I\u00a0will\u00a0remain.

    You can also set padding to a tuple of two integers which will apply padding to the top/bottom and left/right edges. The following example sets padding to (2, 4) which adds two rows to the top and bottom of the widget, and 4 columns to the left and right of the widget.

    padding02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"purple\"\n        self.widget.styles.width = 30\n        self.widget.styles.padding = (2, 4)\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n

    Compare the output of this example to the previous example:

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0 mind-killer. Fear\u00a0is\u00a0the\u00a0 little-death\u00a0that\u00a0 brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0 pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0 past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0 inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0 gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    You can also set padding to a tuple of four values which applies padding to each edge individually. The first value is the padding for the top of the widget, followed by the right of the widget, then bottom, then left.

    "},{"location":"guide/styles/#border","title":"Border","text":"

    The border style draws a border around a widget. To add a border set styles.border to a tuple of two values. The first value is the border type, which should be a string. The second value is the border color which will accept any value that works with color and background.

    The following example adds a border around a widget:

    border01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BorderApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Label(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.border = (\"heavy\", \"yellow\")\n\n\nif __name__ == \"__main__\":\n    app = BorderApp()\n    app.run()\n

    Here is the result:

    BorderApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    There are many other border types. Run the following from the command prompt to preview them.

    textual borders\n
    "},{"location":"guide/styles/#title-alignment","title":"Title alignment","text":"

    Widgets have two attributes, border_title and border_subtitle which (if set) will be displayed within the border. The border_title attribute is displayed in the top border, and border_subtitle is displayed in the bottom border.

    There are two styles to set the alignment of these border labels, which may be set to \"left\", \"right\", or \"center\".

    • border-title-align sets the alignment of the title, which defaults to \"left\".
    • border-subtitle-align sets the alignment of the subtitle, which defaults to \"right\".

    The following example sets both titles and changes the alignment of the title (top) to \"center\".

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BorderTitleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.border = (\"heavy\", \"yellow\")\n        self.widget.border_title = \"Litany Against Fear\"\n        self.widget.border_subtitle = \"by Frank Herbert, in \u201cDune\u201d\"\n        self.widget.styles.border_title_align = \"center\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n

    Note the addition of the titles and their alignments:

    BorderTitleApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Litany\u00a0Against\u00a0Fear\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0by\u00a0Frank\u00a0Herbert,\u00a0in\u00a0\u201cDune\u201d\u00a0\u2501\u251b

    "},{"location":"guide/styles/#outline","title":"Outline","text":"

    Outline is similar to border and is set in the same way. The difference is that outline will not change the size of the widget, and may overlap the content area. The following example sets an outline on a widget:

    outline01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget = Static(TEXT)\n        yield self.widget\n\n    def on_mount(self) -> None:\n        self.widget.styles.background = \"darkblue\"\n        self.widget.styles.width = \"50%\"\n        self.widget.styles.outline = (\"heavy\", \"yellow\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineApp()\n    app.run()\n

    Notice how the outline overlaps the text in the widget.

    OutlineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503ear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503otal\u00a0obliteration.\u2503 \u2503\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503hrough\u00a0me.\u2503 \u2503nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503he\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Outline can be useful to emphasize a widget, but be mindful that it may obscure your content.

    "},{"location":"guide/styles/#box-sizing","title":"Box sizing","text":"

    When you set padding or border it reduces the size of the widget's content area. In other words, setting padding or border won't change the width or height of the widget.

    This is generally desirable when you arrange things on screen as you can add border or padding without breaking your layout. Occasionally though you may want to keep the size of the content area constant and grow the size of the widget to fit padding and border. The box-sizing style allows you to switch between these two modes.

    If you set box_sizing to \"content-box\" then the space required for padding and border will be added to the widget dimensions. The default value of box_sizing is \"border-box\". Compare the box model diagram for content-box to the box model for border-box.

    content-boxborder-box

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT28pcdTAwMTL9nl9Bcb/Gysz0bJ2qV6/Yl1x1MDAxMCAsXHTJq1spYVx1MDAwYizwXHUwMDE2W8bArfz31yNcdTAwMTZJliWwMY6TukpCsEaWRjN9Tp/uWf55s7CwXHUwMDE43XSCxfdcdTAwMGKLwXXVb4S1rj9YfOvOX1x1MDAwNd1e2G5RkYg/99r9bjW+slx1MDAxZUWd3vt375p+9zKIOlxyv1x1MDAxYXhXYa/vN3pRv1x1MDAxNra9arv5LoyCZu+/7ueu31xm/tNpN2tR10tcdTAwMWVSXHRqYdTu3j0raFx1MDAwNM2gXHUwMDE19eju/6PPXHUwMDBiXHUwMDBi/8Q/U7XrXHUwMDA21chvnTeC+Fx1MDAwYnFRUkGpzPDZ3XYrrqxUXFxLieyxPOyt0tOioEaFZ1TjIClxp1x1MDAxNtVJX+2Y48anXHUwMDBmK8tcdTAwMDfXjWZ40lo+Tlx1MDAxZXpcdTAwMTY2XHUwMDFhh9FN464h/Gq9301VqVx1MDAxN3Xbl8GXsFx1MDAxNtWpnFx1MDAwZp1//F6tXHUwMDFkuVxuPFx1MDAxNnfb/fN6K+j1Ml9qd/xqXHUwMDE43bhzLKn/XSO8X0jOXFzTp1xuXGJcdTAwMGa1NVxcXHUwMDFhRW9rRNIg7lx1MDAwNkJ5Qlx1MDAxYtDKMlx1MDAwYkYrXHUwMDA1Q1VbaTeoL6hqf7H4SOp26lcvz6mCrdrjNVHXb/U6fpd6LLlucP/SiqFcdTAwMDfGXG4trFx1MDAwMouQvE89XGLP65HrXHUwMDEzwT1mXHUwMDE1glF3T9NJbYK4Y1x1MDAwNENtuNaQvKWrQ2erXHUwMDE2m8jfw1xyW/e7nfv2W+y5XHUwMDBmqfq7qq9ccttX2sZSnf/j+uvG9tnJPr+48LdVyGorXHUwMDFmLlx1MDAwZVx1MDAxZe+VMUi/221cdTAwMGZcdTAwMTZcdTAwMWZLft7/llSt36n5d2bmXkRIxplcdTAwMTQqeaFG2Lqkwla/0UjOtauXiWXGZ3++nVx1MDAwMFx1MDAxMFx1MDAxYaFcdTAwMTBcdTAwMTCAXHUwMDFjJTPPR0TQ6e5o4P5t31x1MDAxZixvfP+6Jzu6WYCIXpvQPTZcdTAwMWWGvvVcdTAwMTRcdTAwMWPgKTSA8VxiXHUwMDAymlx1MDAxYq1RSc6MzKCBc/C44Fx1MDAxYYAjWSAyVYhcdTAwMDZ1XHUwMDA2taosRcNfsqqDM5VHXHUwMDAyKONZpSTBUuVBIFx1MDAxNHpcdTAwMDJcdLNMXHUwMDAyMDKMXHUwMDFjXGK4Je5ijIOdLVxiqns7X+snm8e1XHUwMDFixFZ/l3dMvVx1MDAxNv6GIJBWXHUwMDE3gYBcYlxuXGZcIpfPXHUwMDA2XHUwMDAxRqfB7sdGY1x1MDAxYsz+5kr9495BuLkxmVtcdTAwMTCFbsHv1afrXHUwMDE2XHUwMDEwPCEtKs6UloZZkcWBXHUwMDA2jyhcdTAwMThAKaG4JNdQiINAXHUwMDFi81x1MDAxMq+Q7vNHXGJwaYdtXHUwMDFlXHUwMDA01YLqNWOT34K1o5Poy97H9kmwXHUwMDFmNcLLz8fV76NNPlxurqOUxb8tu+3h0fopu8L95tGn6HJ5aXn1eFuL5yGp9L5Tr27m6rfPfeCvw32mnmmlamxcdTAwMTHkpSaq1TiG37tcdTAwMTZcdTAwMWSz/vXHSudQXpyurP6wrZrembJcdTAwMTJcdTAwMWPT8z2NeOdWUFgrNaAwRmJcdTAwMDbxgNZcdTAwMDPpXHUwMDA0IEeUWlxmU9H0ZKBcdTAwMTZ5vFx1MDAwYjVcZndcdTAwMGVcbiW5YvNcbjpvmsaYdHq7XHUwMDE1XHUwMDFkhrdBrFEzZ9f9Zti4yfRbbKVU049+9zxspduyXHUwMDE30DOD2Mdnrl5qhOfOjlx1MDAxN1x1MDAxYsFZ1sCjkFx1MDAwMqfH4qidevMqPd2n23W3asNv0e6G9GS/cZStyUTYXHUwMDAyI1xusSWsZEKq57vTpUF978dutHVzc1xm6+2br6dflr75M4yy2ITgXCLhiNJyi1JcdTAwMDCyrDtcdTAwMDVhXHR65G+F0lrSP/Nq6EpcdNoydGkjKVx1MDAxYzMptz9cdTAwMTNvurRdw9Wr3dMts1x1MDAwNlx1MDAxZjuDXHK5PMDOL1x1MDAxM5Avw+6+X6uFrfN5XHUwMDAw70NVJvOMglx1MDAwZp99QC93XHUwMDAyUY+RXCIpl1x1MDAxZvNcbl5yfSVaWFxi7YlcdTAwMTlpYTlCXHUwMDBii9Tz7tHLhdRcbq36g3wj5PC1QsVUq1x1MDAwNWorfzTIcDTIqvStoFtcdTAwMDKzZlirpVx1MDAwM8Ms0p5cbuiGwZepZylcdTAwMDLLY1IsXHUwMDE0qFxcOM/CpFx1MDAxNM9cdTAwMDZitX/bWPmxXHUwMDE0+T40XHUwMDBlj1pcdTAwMWbPw5NOrVx1MDAwMIjVbrvXq9T9qFovXHUwMDAyoyxcdTAwMDLj1FWqS9BQkIekQJXT5CqDRc6ZR1wilYFVxlpGTqxcdTAwMTCLz8jPlGLx6Vx1MDAxY1xyXHUwMDEyXHUwMDE56Lxv1Vx1MDAxNtEgn3F+ctnfq96eXW037GF7XHUwMDBm+X57U/k4hYCyxj5/qHZPbtesXHUwMDFhyNslXFw7XHK7dj5TPnfPXHUwMDFmXHUwMDA1rZLgT3AgzjdqXGYnV97UY2OrMOkzdWxxMmlkoLlLsFx1MDAxYiVTuVx1MDAxNHdcdTAwMDPJOVx1MDAxNVtcdTAwMGLUXHUwMDFhWnElhlE/PZVcbpw8LpJcdTAwMWIzXHUwMDFjJLd6xFBcdTAwMDBYj0JRbUFw61x1MDAwNLVcdTAwMWPGXHUwMDE5xalWKlx1MDAxNJMgLa7qrL1gL/K70XLYiqXa+1x1MDAxNNjIXHUwMDEzVvtxt3qMSWRKWmpdgVxmXHUwMDEznC2e+524kz1uUFx1MDAxYdTuMomJOFh4XHUwMDFjK7vzYitwc7Mp+ss7XHUwMDFi+uhi/Zj8WCNYekDnI+ZcdTAwMTeDVq20Slx1MDAxNeZRXGKHwFx1MDAxNZBp0N8kdnmslPBcdTAwMTCpOiCQrjNaK1NUqdFuKVepht+LVtrNZuiU3n47bEXDTVx1MDAxY7flklx1MDAwM3w98HP6mF4qXTbMXGZcdTAwMWR3xyyjJr8tJKCJPzz+/vfbkVdcdTAwMTeasjsqOStObvcm/f+4op2DKkxhc1x0zKF7XGZKXHUwMDFibSwzpbTJpLvlnlx1MDAxMZI4nNpfMMwltdBcdTAwMTOCsKKEJTzxYrXw4qRW0lx1MDAxYiVht+JEwZKzXHUwMDE5R90v0Fx1MDAwNq/gw8eJXG7yUfdyu1tLi/tfXHUwMDE3dN/XZDJJwpUsnJjA3aBcdTAwMWaF3qlBqqfwW67SpjNcdTAwMDY1deyCXHUwMDE0XHUwMDFlkMKnuJskPcEziVx1MDAwMe/kXGLzNLNGXHUwMDEzsIFcdTAwMWPecEJgeuAlXHUwMDE3p7R2ro34W1oxXCJBTSxPjKtBMpD0x/JcXFAuXGYyhlx1MDAwMn9TNTLsP59cdTAwMTJcdTAwMDWGoTVGS01q0VKclFx1MDAxM1x1MDAwNdazmjqOSJm5aTYyrWX+dE1QaFDuqORtaUxRUMwqXHUwMDE0y1x1MDAxNFx1MDAwNjqMXHUwMDE0PSGNP19cdTAwMTWUz3mZY1bhhFxyXHUwMDE0SiphWHaCh+SS7NJYRnJNkVx1MDAwNac4dvqsotHB06lngopMJf3TtGKssFx1MDAwNFx1MDAxMUZcdTAwMTdwqfNRXHUwMDBlt4bCXHUwMDAxnCRT/9vxXG7ziClcdTAwMTSBQ3CjiFaS5khcdTAwMDKgXCJcdTAwMWFcdTAwMTk9O+83p5FcIlx1MDAwYnJH3nbGpJFYNo1gXHUwMDExhOJMpDUomLYqueIpXHUwMDEy0cd0Tn7b3T9cdTAwMWaovY3o1lZcdTAwMDbt7mQkMruxcsmUZ610Q+HkwFxmXHUwMDFmykJq4Vx1MDAxMblQkVx1MDAxNKhcZvLXXHUwMDFizpPoWdTU4kZcbklcdTAwMDYgR1xm7zGPWSlcdTAwMDGsJDVlNaRH9Vx1MDAxZcSJdrGRSc0km1x0iVx1MDAxMM+mXHUwMDA2l16DRLI3my62M2VTXHUwMDA1dnGvuqMyokOnXHUwMDA0bVxyavjsI7RcdTAwMTGIbUhrPz/qODw+Xr3+hD++NU5cdTAwMDbtlVUxOPig+nNcdTAwMGZtLjwmjFFcdTAwMWFJXHUwMDA0gM7mQIH0gbGK2oFcdTAwMTSEpk+vlzKQpJBcdTAwMTVX6Vx1MDAxMYRcdTAwMTSkIVx1MDAxNo9mxLxcdTAwMThO6Fx1MDAxMnbWUFx1MDAxNoCQxDz/QvnhyPeiOypJXHUwMDA3Tkvbo8XhsylpT0EygHq+tF/fXGYvwP9yeHKw9v3G9PR688BuzT10XHUwMDAxPDBAYVxmMi7lkFems1x1MDAxZfltyzl55Hio/lx1MDAxNaW9Mlx1MDAwNlx1MDAxMJVcdTAwMDYg8YVyRPaPOF5cdTAwMTC9W+DGpfRVzidzelx1MDAwN6TXkDNW9lxc8pQpTWX8XCIjxPXF8vbpkeZXl37YPFx1MDAxZfRcdTAwMWHLXHUwMDExT09cdTAwMWRNZ1x1MDAxMShcdTAwMDJcdTAwMTbIKFZcdTAwMDOOViPafFx1MDAxZYFTME2UJ8m6XHUwMDE1oHrAU8FcYsZrksirav1KsU3FxTlzmlx1MDAxNq9wLorlvlx1MDAxMoxcdTAwMTGNyedrgsp1/fvR5dbFWuvbh481pj/rw09zPzVWUiylwFquSbNcIiP1lWVcdTAwMTZccp5QXHUwMDAy3ChcdTAwMGXxrFx1MDAxNq+3Ror8XHUwMDA2Z4JcZl4yN8/BwKj5fORzgOojjXGLXHUwMDE1VC5nXHUwMDAwZCDaoJpgqvyLiIXE6lxcXHUwMDEwXHUwMDBi8zhDNMqNgVx0hdqkoPSYndQgUbhoXHUwMDE2uFL4h7JKiTm5Y9iQxuSUosFJa1xus5DWcCZcdTAwMTlcdTAwMWIjXHRZ3uvzSihMeU5cdTAwMTRKaS1YJYaUikBPXHUwMDEzj1x1MDAxM4ujdvmU11MqKlx1MDAwMWTJuCRIt1x1MDAxYc6KXHRiipeMS5b7ioypjTVnqVxc3Jbe98HuS2hutuOdm3e9ljKAXzXeeV+TUkYoyjtcdTAwMTBcdTAwMWZcdTAwMTdSQiwySM2PkXhY2j483Fplllx1MDAxZKxcdTAwMWT8WGnsbKnjrVx0XHUwMDA3JmbHXHTIPa1Rc5JcdTAwMTkk45TKJlx1MDAxZVB6XHUwMDFjhdVCXHUwMDE40NrK11t/wz3rVsBcdTAwMDI9xFxyQOmU/EtcdTAwMTSG8khcdTAwMDVZXHUwMDA0i0ZKrUx+XHQquVxmdGNcdTAwMTMzjl6IU8FMgr4/PFx1MDAwZlEp7ta4ON+jYzr7XCJoW1W4+EdwXHUwMDA2XGJcIj2g/uTKOlx1MDAxZV2cXHUwMDFlX0U7387WQrH+5Wrtw+lg3pFNze2RXHUwMDE3Z8LSoVx1MDAxZIqz0HbJIGFcdTAwMTW5e+Pmmr4mtEFow9yQXHUwMDExcVxijExLMM+6OaBWaYoxOeaCh3heoeapqGI2uDYgXne88bfFdUGfxqW57lx1MDAxY1x1MDAxM9UlXHUwMDEzlFThsiDBlFx1MDAwMW7ZXHUwMDE4XHUwMDEzXGZbS2ebn7+sRlx1MDAxYqe3N7jx+Vx1MDAxY7aD02kvXGaa/nxp48JEQ3yqKebnILJcdTAwMTOUXGZRKvlrZrTlbjFcdTAwMWRcdTAwMGVVbHq4ttTLwjDDrTI8PTybXHUwMDFhMDBWulx1MDAwNFx1MDAxYVx1MDAwMFx1MDAxOIGc59bPc0GBl8suzTgrQO2X0lxi089cbny++bz27Wj95OKK0LpcdTAwMTNt3X5a2d0uSDdyQOGmlFx1MDAxOVCMJNaIqcxuzlx1MDAxOTlHaigmKTTmf2y6scik4sK8NU2NVqB4taFVaMmN4vPXXG53t0Cavf7tdme9YTe+Nb/uhLti3lnFLVx1MDAxNSbbc0pcZrQxmF1taKRHoolcdTAwMTlFP5hg/PXEgqWIg7ulZZLiLyPtiEQj97hcdTAwMTVcdTAwMWNccuNuzbKFXHUwMDE0wz2QilIxoGZMKsJcdTAwMWGchlp4MakwjyCkXGJJWjO00pr0eox7SqFWXHUwMDE0JFx1MDAwMJWhdpTMilx1MDAwN+X9p3FKoUG5o5KzpTEppSjZaIrXTYJbiWdcdTAwMTV//mSl8n6fU0LhXHUwMDE2PWDAOVx1MDAxMDzIv1x1MDAwZi3r0tTyUlPUrIxb7FaypdWLk42JPirbe4BcIiVyrVx1MDAwMFx1MDAxM0RcdTAwMTgvyTaWS9CMrY2VbSx3QqX3fTD8XHUwMDEypptttvHOeFNcdTAwMDbwq5KNd1x1MDAxNZlMY5BwK5RcdTAwMThWU4huxtjdq3yjprmdXHUwMDAzXHKeUlJQjOYmobCko+M5TtJlXCKZXHUwMDExQFElXHRcdTAwMTBePJxJN/Lli4YzyYJJSVx1MDAxMztcdItSKVx1MDAxY7U9ifBQkY7Qd9tUMsjHLmi15NJMsunjbzdcdLriVoFyt5OMdfNL2ajhS+2h5syCllTKpEzJ96yQXHUwMDE5vZdBToD8TjKjUmJUcXnensaUXHUwMDFhxdFLSnRcdTAwMGUxXHUwMDBiUiBFfTVcdTAwMDazlO+FM7fMXCI8a4lcdTAwMTZAo0tGXHLNnpTK01x1MDAxMq1LXHUwMDBmoYSS9ZYvJ1x1MDAxNivjdVx1MDAwMlx1MDAxNH0wRcH6qOjFepriSW7BXHUwMDA1llx1MDAxNvJrtrj7srRcdTAwMTNtJjtcdTAwMGa8Mpo9JDOaXkpcdTAwMTjLjeYmtfjoIVwioV5cIk/odlx1MDAwNFaotMoss8jQx+jNuHL08WfEL5Vio3JH3pzGpJXS7V+wZFx1MDAxNlx1MDAxNrqlMEaPseji5lxmev1PW8dw+vX6y/fT6569Piqa3jlX279I5lHrut0nQIGgTshcdTAwMGWluP15rZvFgi5nJYUuli4v3/+Fe6SUXGZcdTAwMTZtXHUwMDAwIzxcdTAwMDBhXHUwMDE41aFgpjZcdTAwMTlcdTAwMTEhkU8yU/vfnWCet1x1MDAxM8yb+5su+p3OYUS3fCRFauuwdlx1MDAxZv8kt1m8XG6DwfKInZrP4sNVOW5cdTAwMDSHkMC19D8/3/z8P1KdJ/cifQ== MarginPaddingContent areaBorderHeightWidth

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9vr+CYr/G2nn0vFJ161x1MDAxNs9cdTAwMTBcdTAwMTJcYpCsgdzaSim2sFx1MDAwNbJlZFx1MDAxMSBb+e+3RybWw5KwjU2c7CqpXHUwMDA0NLLU1vQ5fbrn8fdva2vr8f3AW3+5tu7dtdzAb0fu7fpcdTAwMGJ7/otcdTAwMTdccv2wj00s+X1cdTAwMTjeRK3kym5cdTAwMWNcdTAwMGaGL//4o+dGV148XGLclud88Yc3bjCMb9p+6LTC3lx1MDAxZn7s9Yb/tf9cdTAwMWW6Pe8/g7DXjiMnfUjDa/txXHUwMDE4jZ7lXHUwMDA1Xs/rx0O8+//w97W1v5N/M9ZFXit2+53ASz6QNKVcdTAwMDaCkMWzh2E/MVZIXHUwMDAyyihcdTAwMTi3+8NtfFrstbHxXHUwMDAyLfbSXHUwMDE2e2r9rue/8+NmcPL57IB5XHUwMDE3LvGvXHUwMDA2nfShXHUwMDE3flx1MDAxMLyP74PRi3Bb3ZsoY9IwjsIr79Rvx11sp4Xz48+1w9hcdTAwMWEwbo7Cm06371xyh7lcdTAwMGaFXHUwMDAzt+XH9/ZcdTAwMWMh47Ojl/ByLT1zh79x7nBKtNRCcVwiOOhxq/082GaiOVx1MDAxN1xmXHUwMDE4SC5cdTAwMGKGbYVcdTAwMDH2XHUwMDA0XHUwMDFh9jtJjtSyz27rqoPm9dvja+LI7Vx1MDAwZlx1MDAwN26E/ZVed/vwlVx1MDAwNTFcdTAwMGVXmkmmXHUwMDA114an36br+Z1ubI1h1CFaXHUwMDE4rsToaVx1MDAxOWu8pFskp5wrotm4wZoweN1O/OOv4lvtutHg4eWtXHUwMDBm7S9cdTAwMTnzreU7RefKOlim51xy8TZO7k7J3edBXHUwMDEzneY0eHfahPG9ct7oRlF4uz5u+fbwU2razaDtjnyMSslcdTAwMTlcdTAwMTB8+6D5uD3w+1fY2L9cdIL0XFzYukrdMjn77cVcdTAwMWNokIZVoYFqYjRcdTAwMTDK2NR4ODm97MZcdTAwMWLhtup97Hxccj5+eCvfvb2vwMMwRGzPjIbCp1x1MDAxZVx1MDAwM1x1MDAwM39cZlx1MDAwYpSgt1x1MDAwYsZcYjFcdTAwMDZ9jGb8yH5eoP9RySRBJzXEyKJdKVx1MDAxOMRcdTAwMDVvt6BcdTAwMTZcZr9DS3pcdTAwMTdiXHUwMDEyXGJcXChHXHUwMDBiXHUwMDAxUisxiVx1MDAwMSaMw4yRmiA0XHRcdTAwMTOTXHUwMDE44JxqLkHA82Kg9e7tefds78/2vTH9m0M6UN22/1x1MDAxM2JcdTAwMDA0VGKAUMJcdTAwMDDUXGYxYftcXJxcXDc9ev9n8+xEXHUwMDBm995cdTAwMDSXXHUwMDFmTueLXHSsXG5cdTAwMDVtd9hdbEyg6GTC8jBwXHLAqVB5XHUwMDFjKOooQzlcdTAwMDEjXGZjylTiwJNKPSUooH9PQoBmYtSDzzNcboDxKXv5szj94U5M3vpcdTAwMWRcdTAwMTF/jravTuLgXCLce/Wm3Olj7y7O+PyLutteXGZOXulX/a2d/YbYi09aXHUwMDAxOT6/m1x1MDAwZUtcdTAwMTX3zVnxYtov8uMgmrMzKyiVrkInSCRFaVx1MDAxNJlcdTAwMWGcXHUwMDA3XHUwMDFmVaB29oP30cX713s3w43b1n60YME2Y4iaQq8x6TCigVx1MDAxOcWI4kJCXHUwMDBlm/hcblx1MDAxY661tGKNXHUwMDBiMLxcdTAwMTKbT1x1MDAxNWySTUKTiVwiMlx1MDAwMcMkXG6yZUSjRTpj2ulhP37vf7XvnZHc2V2351x1MDAwN/e5fku81DqSXHUwMDFidfx+9lVcdTAwMGU9fOZIN+Wu3lxi/I714/XAu8g7eOxjfjNujsPMN2/h0128XfS6XfxcdTAwMTZh5OOT3eBD3pK5sMVcdTAwMTWtwlx1MDAxNkXppylcdTAwMTAxfeR7c35w+nZn+PntznHcUTuvOifdT3NGvrmyITJcdTAwMWa6qHFAXHUwMDAxXHUwMDEwXGZrhGNOkkeX4tIxkoNAiYgvg6uloSujMWrQRVxyXHUwMDEzXHUwMDEyreR68fCqXHUwMDBiUFFHXHUwMDFlNFx1MDAwM/r5LrzqkyDUp2Hv4vCHib2ngffIbbf9fmdcdTAwMTXQ+92U+UJj5itcdTAwMTfhK4iNXHUwMDE3POMmj8G3XtmsKnyphDrhisjF5OlZhCuUXGJXllx0xlx1MDAwZvhcdTAwMDVcblx1MDAxOMzR8X+d8MgnXHUwMDEwtoXNaNVcdTAwMWG+K7dcdTAwMWNmplx1MDAxY2Yt/JRcdTAwMTfVXHUwMDAwree329k0Lo+1x7KvXCL8cnbWYrA+gzSqXHUwMDEyiEQxRtksXHUwMDE5ZGfbb59s3HXce/ewXHUwMDE37t1cZm72gFRcdTAwMDCxXHUwMDE1hcNho+vGrW5cdTAwMTVcdTAwMTihXG6MXHUwMDBiXHUwMDE3qkkxXHUwMDA1OCgjXHUwMDA0l0LlXHUwMDBii4JqR6AyJJpQplxyiEooTlFLqYXi4/VcdTAwMTSDykaWXHUwMDA0V8JcdTAwMTlVmstnriY2L11/6+p6p8ng3WB/+PngVu2cPyn7XHUwMDFi3fe8/fVi19/a273YdU9PT10uP7Y+rWaFZvT8MmwhdqrAhXxcdTAwMGWU80xF7DFs1b/pmbFVWaFZOLZcdTAwMThcdTAwMDOHalx1MDAxNKuGIbKooDxcdTAwMDcujSrWgNFcdTAwMDQ1LJGELS9cdOTUYWAoXHUwMDAzRTlQLUuq9txcdTAwMDLdSI1g0txcdTAwMTBcdTAwMDZFnKH1wsBcXCliYulzx8Bh7Ebxpt9PpNrLXGbSMFx1MDAwZbZukk51XGJKXGZMXHUwMDEwNL5cXFsqTkG23nFcdTAwMDdJXHUwMDE3O1x1MDAxNN1VXHUwMDE5aS9cdTAwMDOTdtHaeExrXHUwMDE0w+KtY3bNdjY25T5rvTmmh/y8t/1cdTAwMWSaY8Cve/12rUlccuIwXHJcdTAwMDYlXHUwMDExJ1rjX6kmjGKOMWhcdTAwMGVnXHUwMDA2r1NSXG5VZVR5UJowKnCH8VbY6/lW51x1MDAxZIV+Py6+4uRdbli0dz13Qlx1MDAxZuOXyrZcdTAwMTVpYWDvmKfT9Ke1XHUwMDE0Mskv45//elF6daUn26Mx4cTp7X7L/j+raEfXXHUwMDE3xdNjsYCRx8pYPT2hlTvLs1x1MDAxMtqcwlx1MDAxZDRihitqhLKygFx1MDAxNataysGEXFxK7Fx1MDAxZMy6dbVaeHJVK33ZdXk31cJIhO3PI1xyllx1MDAxMMJnyVxuJvPuzTBqZ8X9j0u7XHUwMDFmLJlPkVBUlpVcYmbSKIXeP/2gab1IW8yA0cLRXHUwMDBiRDtSXHUwMDEwJE4g3GiTwedIjmAzZkVcdTAwMWP9TGnk0KWhXHUwMDE3g5yQ0lx1MDAwNjdkcNCspESNPM/xXHUwMDFhjpaCLVx1MDAxM9CJpNygnVx1MDAxNNRcdTAwMWMltVWQI8VcdTAwMDD6mCpQxGilJGDXXGKt02g0Vlx1MDAwNdrRkmEqRFxmkahUICtmfnVRUOlP9mhMutKMqqCaVLjkxdNjUsHwx7iZZWZS/fyUXHUwMDE1Jlx1MDAxNVx1MDAwMIZZu1accZpGkIRTXHUwMDE4qlWCnlx1MDAwYlRcdTAwMTCioLqU93ROkYZcdTAwMTJI1LN9Xiqcc6SiNNOIXHUwMDEwNFx1MDAwNclDTiQ50tY5XHUwMDA05mT/XHUwMDAwViHYb0YgNFx1MDAxOFVcdTAwMDJJJX1cdTAwMWJp/lNFXCLlk+h+clx1MDAxMqlyIHtMus6MJJJoplx1MDAxMlx1MDAwZdGiejyAII1cdTAwMGL0yOnHynfc+91B0CHNe9o6+nIs2NZ1pz9cdTAwMWaFPN9YOShw8ItcIj2D4sJQmtclglJcdTAwMDdTPWCUXCLhSrk8XVwiMN2XmOyXliCpU6xOfs8yXHUwMDEwPVxcSzPPdMbVJo38zVx1MDAxNovlXFzbQoFcXNKL9lx1MDAxOPffgoCLwKxcdTAwMDIuYlpcYok6e2rcfo1JTM8uL4/5K7155DF+pE/C1cetclx1MDAwNFKmRKFjU6g8bJUmXHUwMDBlalx1MDAwMi2JoFx1MDAxY4XZ8opcdTAwMDGA2ldQkVx1MDAxZFx1MDAxYlx1MDAxOKOWODyRhSWwTSZcdTAwMGZgXHUwMDE2NMeg3r+wTdpcdTAwMTZcbtvJXrRHI+3ARal2wyonp1FJmJI0XHUwMDFik1x1MDAxZkNu97T1seG7uumfiNthXHUwMDE0XGbgar9q4G91kGvszFHCXGZTzNhZNPmBXHTJXHUwMDE0akOpNGWYWGUnky9etVx1MDAxYrt4g9tcdTAwMDVcdTAwMDE2PYCSup5cdTAwMWShxORBg5DKlutFxpxcdTAwMTGUNbdF2J82XHUwMDAwl2f8XHUwMDA0iOBcXFx1MDAxYqFcdTAwMTRXoFVWi49HJ1BcdTAwMTNJ4Fx1MDAxOG64ydVcdTAwMDRyYl1cXEf87O71wac+XHUwMDA0fPPr8aG5fffpkbGJZXLIUmV8o9qlkuZJb1pcdTAwMTSvUKKqS4xIYVxcIK9MX2I87X9yNztcdTAwMWJcdTAwMDfXsblcdTAwMGVft44/6aG6Wn1iUY62Yy+Gg9CSZtZtJcSiqMOMXaYkUVx1MDAxNDC+vFx1MDAxMU9MXHUwMDE5gFx1MDAxM84lXHUwMDEwUJiz8ZJygKHoKlxuXHUwMDEzXHUwMDBlwTRXYrJcdTAwMTiAmFI8y0k/O62gMqaYvFwiWVwiLlxieiOdZFx1MDAxNe1IXHUwMDBlXHUwMDE4XHUwMDE4XGYjnFxuUVlH/EexSrU72aPoSDMyStWoo1bV1UXQXHUwMDA0I7NR009cdTAwMTUs769V51x1MDAxMyRcZoxvmml0VU15fqWX5MqRQlx1MDAwM1x1MDAwM8XxXmp583xTXHUwMDE41I43XHUwMDAyp9RcdTAwMGXaz05cdTAwMThPXHUwMDE5bqyPXHUwMDE0OVebaSZSvbStve93v19cdTAwMDbNzTWMuTfqtYxcdTAwMDP8qGHMXHUwMDA3S2pcdTAwMTmhquaQXHUwMDFioyzOq1wiRlx1MDAxYqpcdTAwMTlMP1x1MDAwZmEn2N30olx1MDAwZsHB9kWjcfl+b/NAdVc+d7GpXHUwMDBiQt1cYm2IMpLnU1x1MDAxN0GoYydvUmaFs1x1MDAxMMurOlx1MDAxMEdcdTAwMTLDkPIxg0LuZ7REYYBwXGJBMapcdTAwMDU2M010SfHQ2HnFdJ6lXHUwMDAxqyAyfrUqRHWv2qMx2aEzxvoqZGeHxlxuwFaCSsblXGZcdTAwMDOJW/os+qT3z/eGTdlvXHUwMDFmeTJqbp6tOq5BMocwzKE0SClcdTAwMDXPZ1x1MDAwZWCsLtVSXHUwMDAyaFBMXHUwMDE2J0gvXHUwMDEy11x1MDAxYWWVlkJJSqTWZSVcdLA7XHUwMDFjoFxmVIKJZMnVXHUwMDA0rDm26rmUwFNRndryL6rHXHUwMDE3VPapPVx1MDAxYVx1MDAxM905I6irS1x1MDAwMjWLXGYwvbKlRlx1MDAwZdOXXHUwMDA0ro/8+y3SNedcdTAwMWa67Y7vn53e7u6t/ChcdTAwMDFcdTAwMDNwXGaVRktjXGLDfLqwdYnmXHUwMDBlxUxTKWqzTbnEcVx1MDAwMlx1MDAwNKVDsEeYpkTR7Gq8zCCf5lx1MDAwMrBdc66YoXRiXHI7xYTDUFwi5plTuFxu0C4vNqKjUtSPJNlMXHUwMDA3aHZcdTAwMDZNWmy0U42JXHUwMDExXHUwMDA0OFx1MDAxM7Sy2rjt9ptnPGTn9+JcdTAwMGJcdTAwMWKqo2PYV41ftS7QqPSpUeuEOy2MWZioLFx1MDAwZVx1MDAxOCWTXYCmXHUwMDE3XGaXYuvd61x1MDAxYlxugygkV1+uXHUwMDA2zd1ccrq/6sRiXHUwMDE3XHUwMDAxK1wiXHUwMDExiShcdTAwMDeULTlcdTAwMTaIhTmMMrthXHUwMDEyXHUwMDAzu6JrebzC7OQ7u9hYKjvJSZcsXG62w1hcdTAwMTQoXHUwMDA2ICGl0lxcZ1x1MDAwMsN3ZtHG2PDzI1KBJTFcdTAwMGJxiCDSXHUwMDBls1x1MDAwMoKEI7WUzDGiXHUwMDBlZVxcM0zXXGJcdTAwMDVM6SqXM/yjmKXaqezRKPGnXHUwMDE5qaWq6qhqJFx1MDAwYrpcdTAwMGJcdTAwMTUzTEcq77FcdTAwMTXnXHUwMDE1JoQjNJN25Vx1MDAwMFxiY1xuq7ZAXHUwMDFhh2qFnopdw1x1MDAxOC9cdTAwMWG2wKJj+uC6oqO09Icq65lcdTAwMTc51IvRnKvNVHWsj0W19/3u98sgu7mqjiPnzTjAjyo6jlxmmU9qQMbFi8NcdTAwMTBUXHUwMDE4JG0mp6851m+atLKTnFxyKjmuXHTlRmCQyqcwXG6sXHUwMDEwwVx1MDAxNIeB3dOnZoJcIlx1MDAwN+7Ck2pcdTAwMTNcdTAwMDSNtSNRo9XYwpTtP8JcdTAwMWMjXHUwMDA0hlx1MDAwNTtcdTAwMDVcdTAwMGWN5ZM5XGbmWUJcdTAwMTIxzz5cXKugNGZbPGGXeVKN6krb7TUwnGaS/1x1MDAwN1xyYreLsVwiXVx1MDAwMrZcdTAwMTKAjIovSJDSrVxuJiTIzyQ0XHUwMDFhNU6VtE/604xKozqJ0ZVFT8G5xD9meq1Rv9nNXG5cdTAwMTNcdTAwMGKVnNi1lPhfcVxupeCONFx1MDAwNLVcYv4k6qZQPp1YNCQrXHUwMDAxJNGWXHUwMDE3oIRX7PIwRmzJSlx1MDAxOM3snKtcIq/gWck5Uz9gifiySiPUQaEnQWNcdTAwMTKjqZJUlVRGhGPXXHUwMDFlUVBcYlx1MDAxZCFFblx1MDAxZEWOPco325pgj18jgWlU+5Q9Jr1pRlap3dzFsMokxqC254ZPTyyt4y+dm6NBcDxodT9s0r1cdTAwMGZfL5tVeyyt2NYu0tFcbr8tui9nupDH2G1yXHI6LbW+bZipnjzx9K1dqMOBVi2sYII5ljUsXHUwMDAzlk/VtjsoamaL88+b4vy8W7zMQoe/fb9xctN1dzB4XHUwMDFm4y3HhIjv2m8/pD7pbda/+N7tZsmGyVx1MDAxN8lhTU5egsWHZ9/0399++/Z/3Vx1MDAxNVx1MDAwZVx1MDAwMiJ9 MarginPaddingContent areaBorderHeightWidth

    The following example creates two widgets with a width of 30, a height of 6, and a border and padding of 1. The first widget has the default box_sizing (\"border-box\"). The second widget sets box_sizing to \"content-box\".

    box_sizing01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass BoxSizing(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.width = 30\n        self.widget2.styles.width = 30\n        self.widget1.styles.height = 6\n        self.widget2.styles.height = 6\n        self.widget1.styles.border = (\"heavy\", \"white\")\n        self.widget2.styles.border = (\"heavy\", \"white\")\n        self.widget1.styles.padding = 1\n        self.widget2.styles.padding = 1\n        self.widget2.styles.box_sizing = \"content-box\"\n\n\nif __name__ == \"__main__\":\n    app = BoxSizing()\n    app.run()\n

    The padding and border of the first widget is subtracted from the height leaving only 2 lines in the content area. The second widget also has a height of 6, but the padding and border adds additional height so that the content area remains 6 lines.

    BoxSizing \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u2503 \u2503brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    "},{"location":"guide/styles/#margin","title":"Margin","text":"

    Margin is similar to padding in that it adds space, but unlike padding, margin is outside of the widget's border. It is used to add space between widgets.

    The following example creates two widgets, each with a margin of 2.

    margin01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass MarginApp(App):\n    def compose(self) -> ComposeResult:\n        self.widget1 = Static(TEXT)\n        yield self.widget1\n        self.widget2 = Static(TEXT)\n        yield self.widget2\n\n    def on_mount(self) -> None:\n        self.widget1.styles.background = \"purple\"\n        self.widget2.styles.background = \"darkgreen\"\n        self.widget1.styles.border = (\"heavy\", \"white\")\n        self.widget2.styles.border = (\"heavy\", \"white\")\n        self.widget1.styles.margin = 2\n        self.widget2.styles.margin = 2\n\n\nif __name__ == \"__main__\":\n    app = MarginApp()\n    app.run()\n

    Notice how each widget has an additional two rows and columns around the border.

    MarginApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    Note

    In the above example both widgets have a margin of 2, but there are only 2 lines of space between the widgets. This is because margins of consecutive widgets overlap. In other words when there are two widgets next to each other Textual picks the greater of the two margins.

    "},{"location":"guide/styles/#more-styles","title":"More styles","text":"

    We've covered the most fundamental styles used by Textual apps, but there are many more which you can use to customize many aspects of how your app looks. See the Styles reference for a comprehensive list.

    In the next chapter we will discuss Textual CSS which is a powerful way of applying styles to widgets that keeps your code free of style attributes.

    "},{"location":"guide/testing/","title":"Testing","text":"

    Code testing is an important part of software development. This chapter will cover how to write tests for your Textual apps.

    "},{"location":"guide/testing/#what-is-testing","title":"What is testing?","text":"

    It is common to write tests alongside your app. A test is simply a function that confirms your app is working correctly.

    Learn more about testing

    We recommend Python Testing with pytest for a comprehensive guide to writing tests.

    "},{"location":"guide/testing/#do-you-need-to-write-tests","title":"Do you need to write tests?","text":"

    The short answer is \"no\", you don't need to write tests.

    In practice however, it is almost always a good idea to write tests. Writing code that is completely bug free is virtually impossible, even for experienced developers. If you want to have confidence that your application will run as you intended it to, then you should write tests. Your test code will help you find bugs early, and alert you if you accidentally break something in the future.

    "},{"location":"guide/testing/#testing-frameworks-for-textual","title":"Testing frameworks for Textual","text":"

    Textual doesn't require any particular test framework. You can use any test framework you are familiar with, but we will be using pytest in this chapter.

    "},{"location":"guide/testing/#testing-apps","title":"Testing apps","text":"

    You can often test Textual code in the same way as any other app, and use similar techniques. But when testing user interface interactions, you may need to use Textual's dedicated test features.

    Let's write a simple Textual app so we can demonstrate how to test it. The following app shows three buttons labelled \"red\", \"green\", and \"blue\". Clicking one of those buttons or pressing a corresponding R, G, and B key will change the background color.

    rgb.pyOutput
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Footer\n\n\nclass RGBApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Horizontal {\n        width: auto;\n        height: auto;\n    }\n    \"\"\"\n\n    BINDINGS = [\n        (\"r\", \"switch_color('red')\", \"Go Red\"),\n        (\"g\", \"switch_color('green')\", \"Go Green\"),\n        (\"b\", \"switch_color('blue')\", \"Go Blue\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Button(\"Red\", id=\"red\")\n            yield Button(\"Green\", id=\"green\")\n            yield Button(\"Blue\", id=\"blue\")\n        yield Footer()\n\n    @on(Button.Pressed)\n    def pressed_button(self, event: Button.Pressed) -> None:\n        assert event.button.id is not None\n        self.action_switch_color(event.button.id)\n\n    def action_switch_color(self, color: str) -> None:\n        self.screen.styles.background = color\n\n\nif __name__ == \"__main__\":\n    app = RGBApp()\n    app.run()\n

    RGBApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 RedGreenBlue \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0R\u00a0\u00a0Go\u00a0Red\u00a0\u00a0G\u00a0\u00a0Go\u00a0Green\u00a0\u00a0B\u00a0\u00a0Go\u00a0Blue\u00a0

    Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code. Tests allow us to automate such testing so we can quickly simulate user interactions and check the result.

    To test our simple app we will use the run_test() method on the App class. This replaces the usual call to run() and will run the app in headless mode, which prevents Textual from updating the terminal but otherwise behaves as normal.

    The run_test() method is an async context manager which returns a Pilot object. You can use this object to interact with the app as if you were operating it with a keyboard and mouse.

    Let's look at the tests for the example above:

    test_rgb.py
    from rgb import RGBApp\n\nfrom textual.color import Color\n\n\nasync def test_keys():  # (1)!\n    \"\"\"Test pressing keys has the desired result.\"\"\"\n    app = RGBApp()\n    async with app.run_test() as pilot:  # (2)!\n        # Test pressing the R key\n        await pilot.press(\"r\")  # (3)!\n        assert app.screen.styles.background == Color.parse(\"red\")  # (4)!\n\n        # Test pressing the G key\n        await pilot.press(\"g\")\n        assert app.screen.styles.background == Color.parse(\"green\")\n\n        # Test pressing the B key\n        await pilot.press(\"b\")\n        assert app.screen.styles.background == Color.parse(\"blue\")\n\n        # Test pressing the X key\n        await pilot.press(\"x\")\n        # No binding (so no change to the color)\n        assert app.screen.styles.background == Color.parse(\"blue\")\n\n\nasync def test_buttons():\n    \"\"\"Test pressing keys has the desired result.\"\"\"\n    app = RGBApp()\n    async with app.run_test() as pilot:\n        # Test clicking the \"red\" button\n        await pilot.click(\"#red\")  # (5)!\n        assert app.screen.styles.background == Color.parse(\"red\")\n\n        # Test clicking the \"green\" button\n        await pilot.click(\"#green\")\n        assert app.screen.styles.background == Color.parse(\"green\")\n\n        # Test clicking the \"blue\" button\n        await pilot.click(\"#blue\")\n        assert app.screen.styles.background == Color.parse(\"blue\")\n
    1. The run_test() method requires that it run in a coroutine, so tests must use the async keyword.
    2. This runs the app and returns a Pilot instance we can use to interact with it.
    3. Simulates pressing the R key.
    4. This checks that pressing the R key has resulted in the background color changing.
    5. Simulates clicking on the widget with an id of red (the button labelled \"Red\").

    There are two tests defined in test_rgb.py. The first to test keys and the second to test button clicks. Both tests first construct an instance of the app and then call run_test() to get a Pilot object. The test_keys function simulates key presses with Pilot.press, and test_buttons simulates button clicks with Pilot.click.

    After simulating a user interaction, Textual tests will typically check the state has been updated with an assert statement. The pytest module will record any failures of these assert statements as a test fail.

    If you run the tests with pytest test_rgb.py you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color.

    If you later update this app, and accidentally break this functionality, one or more of your tests will fail. Knowing which test has failed will help you quickly track down where your code was broken.

    "},{"location":"guide/testing/#simulating-key-presses","title":"Simulating key presses","text":"

    We've seen how the press method simulates keys. You can also supply multiple keys to simulate the user typing in to the app. Here's an example of simulating the user typing the word \"hello\".

    await pilot.press(\"h\", \"e\", \"l\", \"l\", \"o\")\n

    Each string creates a single keypress. You can also use the name for non-printable keys (such as \"enter\") and the \"ctrl+\" modifier. These are the same identifiers as used for key events, which you can experiment with by running textual keys.

    "},{"location":"guide/testing/#simulating-clicks","title":"Simulating clicks","text":"

    You can simulate mouse clicks in a similar way with Pilot.click. If you supply a CSS selector Textual will simulate clicking on the matching widget.

    Note

    If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector. This is generally what you want, because a real user would experience the same thing.

    "},{"location":"guide/testing/#clicking-the-screen","title":"Clicking the screen","text":"

    If you don't supply a CSS selector, then the click will be relative to the screen. For example, the following simulates a click at (0, 0):

    await pilot.click()\n
    "},{"location":"guide/testing/#click-offsets","title":"Click offsets","text":"

    If you supply an offset value, it will be added to the coordinates of the simulated click. For example the following line would simulate a click at the coordinates (10, 5).

    await pilot.click(offset=(10, 5))\n

    If you combine this with a selector, then the offset will be relative to the widget. Here's how you would click the line above a button.

    await pilot.click(Button, offset=(0, -1))\n
    "},{"location":"guide/testing/#modifier-keys","title":"Modifier keys","text":"

    You can simulate clicks in combination with modifier keys, by setting the shift, meta, or control parameters. Here's how you could simulate ctrl-clicking a widget with an ID of \"slider\":

    await pilot.click(\"#slider\", control=True)\n
    "},{"location":"guide/testing/#changing-the-screen-size","title":"Changing the screen size","text":"

    The default size of a simulated app is (80, 24). You may want to test what happens when the app has a different size. To do this, set the size parameter of run_test to a different size. For example, here is how you would simulate a terminal resized to 100 columns and 50 lines:

    async with app.run_test(size=(100, 50)) as pilot:\n    ...\n
    "},{"location":"guide/testing/#pausing-the-pilot","title":"Pausing the pilot","text":"

    Some actions in a Textual app won't change the state immediately. For instance, messages may take a moment to bubble from the widget that sent them. If you were to post a message and immediately assert you may find that it fails because the message hasn't yet been processed.

    You can generally solve this by calling pause() which will wait for all pending messages to be processed. You can also supply a delay parameter, which will insert a delay prior to waiting for pending messages.

    "},{"location":"guide/testing/#textuals-tests","title":"Textual's tests","text":"

    Textual itself has a large battery of tests. If you are interested in how we write tests, see the tests/ directory in the Textual repository.

    "},{"location":"guide/testing/#snapshot-testing","title":"Snapshot testing","text":"

    Snapshot testing is the process of recording the output of a test, and comparing it against the output from previous runs.

    Textual uses snapshot testing internally to ensure that the builtin widgets look and function correctly in every release. We've made the pytest plugin we built available for public use.

    The official Textual pytest plugin can help you catch otherwise difficult to detect visual changes in your app.

    It works by generating an SVG screenshot (such as the images in these docs) from your app. If the screenshot changes in any test run, you will have the opportunity to visually compare the new output against previous runs.

    "},{"location":"guide/testing/#installing-the-plugin","title":"Installing the plugin","text":"

    You can install pytest-textual-snapshot using your favorite package manager (pip, poetry, etc.).

    pip install pytest-textual-snapshot\n
    "},{"location":"guide/testing/#creating-a-snapshot-test","title":"Creating a snapshot test","text":"

    With the package installed, you now have access to the snap_compare pytest fixture.

    Let's look at an example of how we'd create a snapshot test for the calculator app below.

    CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 C+/-%\u00f7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 789\u00d7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 456- \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 123+ \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 0.= \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    First, we need to create a new test and specify the path to the Python file containing the app. This path should be relative to the location of the test.

    def test_calculator(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\")\n

    Let's run the test as normal using pytest.

    pytest\n

    When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail. Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to.

    If you open the snapshot report in your browser, you'll see something like this:

    Tip

    You can usually open the link directly from the terminal, but some terminal emulators may require you to hold Ctrl or Cmd while clicking for links to work.

    The report explains that there's \"No history for this test\". It's our job to validate that the initial snapshot looks correct before proceeding. Our calculator is rendering as we expect, so we'll save this snapshot:

    pytest --snapshot-update\n

    Warning

    Only ever run pytest with --snapshot-update if you're happy with how the output looks on the left hand side of the snapshot report. When using --snapshot-update, you're saying \"I'm happy with all of the screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared against\". As such, you should only run pytest --snapshot-update after running pytest and confirming the output looks good.

    Now that our snapshot is saved, if we run pytest (with no arguments) again, the test will pass. This is because the screenshot taken during this test run matches the one we saved earlier.

    "},{"location":"guide/testing/#catching-a-bug","title":"Catching a bug","text":"

    The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed.

    Imagine a new developer joins your team, and tries to make a few changes to the calculator. While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app. When they run pytest, they're presented with a report which reveals the damage:

    On the right, we can see our \"historical\" snapshot - this is the one we saved earlier. On the left is how our app is currently rendering - clearly not how we intended!

    We can click the \"Show difference\" toggle at the top right of the diff to overlay the two versions:

    This reveals another problem, which could easily be missed in a quick visual inspection - our new developer has also deleted the number 4!

    Tip

    Snapshot tests work well in CI on all supported operating systems, and the snapshot report is just an HTML file which can be exported as a build artifact.

    "},{"location":"guide/testing/#pressing-keys","title":"Pressing keys","text":"

    You can simulate pressing keys before the snapshot is captured using the press parameter.

    def test_calculator_pressing_numbers(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\", press=[\"1\", \"2\", \"3\"])\n
    "},{"location":"guide/testing/#changing-the-terminal-size","title":"Changing the terminal size","text":"

    To capture the snapshot with a different terminal size, pass a tuple (width, height) as the terminal_size parameter.

    def test_calculator(snap_compare):\n    assert snap_compare(\"path/to/calculator.py\", terminal_size=(50, 100))\n
    "},{"location":"guide/testing/#running-setup-code","title":"Running setup code","text":"

    You can also run arbitrary code before the snapshot is captured using the run_before parameter.

    In this example, we use run_before to hover the mouse cursor over the widget with ID number-5 before taking the snapshot.

    def test_calculator_hover_number(snap_compare):\n    async def run_before(pilot) -> None:\n        await pilot.hover(\"#number-5\")\n\n    assert snap_compare(\"path/to/calculator.py\", run_before=run_before)\n

    For more information, visit the pytest-textual-snapshot repo on GitHub.

    "},{"location":"guide/widgets/","title":"Widgets","text":"

    In this chapter we will explore widgets in more detail, and how you can create custom widgets of your own.

    "},{"location":"guide/widgets/#what-is-a-widget","title":"What is a widget?","text":"

    A widget is a component of your UI responsible for managing a rectangular region of the screen. Widgets may respond to events in much the same way as an app. In many respects, widgets are like mini-apps.

    Information

    Every widget runs in its own asyncio task.

    "},{"location":"guide/widgets/#custom-widgets","title":"Custom widgets","text":"

    There is a growing collection of builtin widgets in Textual, but you can build entirely custom widgets that work in the same way.

    The first step in building a widget is to import and extend a widget class. This can either be Widget which is the base class of all widgets, or one of its subclasses.

    Let's create a simple custom widget to display a greeting.

    hello01.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\n\n\nclass Hello(Widget):\n    \"\"\"Display a greeting.\"\"\"\n\n    def render(self) -> RenderResult:\n        return \"Hello, [b]World[/b]!\"\n\n\nclass CustomApp(App):\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n

    The highlighted lines define a custom widget class with just a render() method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.

    Note that the text contains tags in square brackets, i.e. [b]. This is console markup which allows you to embed various styles within your content. If you run this you will find that World is in bold.

    CustomApp Hello,\u00a0World!

    This (very simple) custom widget may be styled in the same way as builtin widgets, and targeted with CSS. Let's add some CSS to this app.

    hello02.pyhello02.tcss hello02.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\n\n\nclass Hello(Widget):\n    \"\"\"Display a greeting.\"\"\"\n\n    def render(self) -> RenderResult:\n        return \"Hello, [b]World[/b]!\"\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello02.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    color: $text;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    The addition of the CSS has completely transformed our custom widget.

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHello,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/widgets/#static-widget","title":"Static widget","text":"

    While you can extend the Widget class, a subclass will typically be a better starting point. The Static class is a widget subclass which caches the result of render, and provides an update() method to update the content area.

    Let's use Static to create a widget which cycles through \"hello\" in various languages.

    hello03.pyhello03.tcssOutput hello03.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    def on_mount(self) -> None:\n        self.next_word()\n\n    def on_click(self) -> None:\n        self.next_word()\n\n    def next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"{hello}, [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello03.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello03.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note that there is no render() method on this widget. The Static class is handling the render for us. Instead we call update() when we want to update the content within the widget.

    The next_word method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget.

    "},{"location":"guide/widgets/#default-css","title":"Default CSS","text":"

    When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a DEFAULT_CSS class variable inside your widget class.

    Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code.

    Here's the Hello example again, this time the widget has embedded default CSS:

    hello04.pyhello04.tcssOutput hello04.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Hello {\n        width: 40;\n        height: 9;\n        padding: 1 2;\n        background: $panel;\n        border: $secondary tall;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    def on_mount(self) -> None:\n        self.next_word()\n\n    def on_click(self) -> None:\n        self.next_word()\n\n    def next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"{hello}, [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello04.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello04.tcss
    Screen {\n    align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"guide/widgets/#scoped-css","title":"Scoped CSS","text":"

    Default CSS is scoped by default. All this means is that CSS defined in DEFAULT_CSS will affect the widget and potentially its children only. This is to prevent you from inadvertently breaking an unrelated widget.

    You can disable scoped CSS by setting the class var SCOPED_CSS to False.

    "},{"location":"guide/widgets/#default-specificity","title":"Default specificity","text":"

    CSS defined within DEFAULT_CSS has an automatically lower specificity than CSS read from either the App's CSS class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.

    "},{"location":"guide/widgets/#text-links","title":"Text links","text":"

    Text in a widget may be marked up with links which perform an action when clicked. Links in console markup use the following format:

    \"Click [@click='app.bell']Me[/]\"\n

    The @click tag introduces a click handler, which runs the app.bell action.

    Let's use markup links in the hello example so that the greeting becomes a link which updates the widget.

    hello05.pyhello05.tcssOutput hello05.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    def on_mount(self) -> None:\n        self.action_next_word()\n\n    def action_next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello05.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    hello05.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the next_word action which updates the next word.

    "},{"location":"guide/widgets/#border-titles","title":"Border titles","text":"

    Every widget has a border_title and border_subtitle attribute. Setting border_title will display text within the top border, and setting border_subtitle will display text within the bottom border.

    Note

    Border titles will only display if the widget has a border enabled.

    The default value for these attributes is empty string, which disables the title. You can change the default value for the title attributes with the BORDER_TITLE and BORDER_SUBTITLE class variables.

    Let's demonstrate setting a title, both as a class variable and a instance variable:

    hello06.pyhello06.tcssOutput hello06.py
    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nhellos = cycle(\n    [\n        \"Hola\",\n        \"Bonjour\",\n        \"Guten tag\",\n        \"Salve\",\n        \"N\u01d0n h\u01ceo\",\n        \"Ol\u00e1\",\n        \"Asalaam alaikum\",\n        \"Konnichiwa\",\n        \"Anyoung haseyo\",\n        \"Zdravstvuyte\",\n        \"Hello\",\n    ]\n)\n\n\nclass Hello(Static):\n    \"\"\"Display a greeting.\"\"\"\n\n    BORDER_TITLE = \"Hello Widget\"  # (1)!\n\n    def on_mount(self) -> None:\n        self.action_next_word()\n        self.border_subtitle = \"Click for next hello\"  # (2)!\n\n    def action_next_word(self) -> None:\n        \"\"\"Get a new hello and update the content area.\"\"\"\n        hello = next(hellos)\n        self.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\n\n\nclass CustomApp(App):\n    CSS_PATH = \"hello05.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Hello()\n\n\nif __name__ == \"__main__\":\n    app = CustomApp()\n    app.run()\n
    1. Setting the default for the title attribute via class variable.
    2. Setting subtitle via an instance attribute.
    hello06.tcss
    Screen {\n    align: center middle;\n}\n\nHello {\n    width: 40;\n    height: 9;\n    padding: 1 2;\n    background: $panel;\n    border: $secondary tall;\n    content-align: center middle;\n}\n

    CustomApp \u258a\u2594\u00a0Hello\u00a0Widget\u00a0\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Click\u00a0for\u00a0next\u00a0hello\u00a0\u2581\u258e

    Note that titles are limited to a single line of text. If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added).

    There are a number of styles that influence how titles are displayed (color and alignment). See the style reference for details.

    "},{"location":"guide/widgets/#rich-renderables","title":"Rich renderables","text":"

    In previous examples we've set strings as content for Widgets. You can also use special objects called renderables for advanced visuals. You can use any renderable defined in Rich or third party libraries.

    Lets make a widget that uses a Rich table for its content. The following app is a solution to the classic fizzbuzz problem often used to screen software engineers in job interviews. The problem is this: Count up from 1 to 100, when the number is divisible by 3, output \"fizz\"; when the number is divisible by 5, output \"buzz\"; and when the number is divisible by both 3 and 5 output \"fizzbuzz\".

    This app will \"play\" fizz buzz by displaying a table of the first 15 numbers and columns for fizz and buzz.

    fizzbuzz01.pyfizzbuzz01.tcssOutput fizzbuzz01.py
    from rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass FizzBuzz(Static):\n    def on_mount(self) -> None:\n        table = Table(\"Number\", \"Fizz?\", \"Buzz?\")\n        for n in range(1, 16):\n            fizz = not n % 3\n            buzz = not n % 5\n            table.add_row(\n                str(n),\n                \"fizz\" if fizz else \"\",\n                \"buzz\" if buzz else \"\",\n            )\n        self.update(table)\n\n\nclass FizzBuzzApp(App):\n    CSS_PATH = \"fizzbuzz01.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield FizzBuzz()\n\n\nif __name__ == \"__main__\":\n    app = FizzBuzzApp()\n    app.run()\n
    fizzbuzz01.tcss
    Screen {\n    align: center middle;\n}\n\nFizzBuzz {\n    width: auto;\n    height: auto;\n    background: $primary;\n    color: $text;\n}\n

    FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u2503Fizz?\u2503Buzz?\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502buzz\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    "},{"location":"guide/widgets/#content-size","title":"Content size","text":"

    Textual will auto-detect the dimensions of the content area from rich renderables if width or height is set to auto. You can override auto dimensions by implementing get_content_width() or get_content_height().

    Let's modify the default width for the fizzbuzz example. By default, the table will be just wide enough to fix the columns. Let's force it to be 50 characters wide.

    fizzbuzz02.pyfizzbuzz02.tcssOutput fizzbuzz02.py
    from rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.widgets import Static\n\n\nclass FizzBuzz(Static):\n    def on_mount(self) -> None:\n        table = Table(\"Number\", \"Fizz?\", \"Buzz?\", expand=True)\n        for n in range(1, 16):\n            fizz = not n % 3\n            buzz = not n % 5\n            table.add_row(\n                str(n),\n                \"fizz\" if fizz else \"\",\n                \"buzz\" if buzz else \"\",\n            )\n        self.update(table)\n\n    def get_content_width(self, container: Size, viewport: Size) -> int:\n        \"\"\"Force content width size.\"\"\"\n        return 50\n\n\nclass FizzBuzzApp(App):\n    CSS_PATH = \"fizzbuzz02.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield FizzBuzz()\n\n\nif __name__ == \"__main__\":\n    app = FizzBuzzApp()\n    app.run()\n
    fizzbuzz02.tcss
    Screen {\n    align: center middle;\n}\n\nFizzBuzz {\n    width: auto;\n    height: auto;\n    background: $primary;\n    color: $text;\n}\n

    FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Fizz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Buzz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    Note that we've added expand=True to tell the Table to expand beyond the optimal width, so that it fills the 50 characters returned by get_content_width.

    "},{"location":"guide/widgets/#tooltips","title":"Tooltips","text":"

    Widgets can have tooltips which is content displayed when the user hovers the mouse over the widget. You can use tooltips to add supplementary information or help messages.

    Tip

    It is best not to rely on tooltips for essential information. Some users prefer to use the keyboard exclusively and may never see tooltips.

    To add a tooltip, assign to the widget's tooltip property. You can set text or any other Rich renderable.

    The following example adds a tooltip to a button:

    tooltip01.pyOutput (before hover)Output (after hover) tooltip01.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\n\n\nclass TooltipApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"Click me\", variant=\"success\")\n\n    def on_mount(self) -> None:\n        self.query_one(Button).tooltip = TEXT\n\n\nif __name__ == \"__main__\":\n    app = TooltipApp()\n    app.run()\n

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    TooltipApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/widgets/#customizing-the-tooltip","title":"Customizing the tooltip","text":"

    If you don't like the default look of the tooltips, you can customize them to your liking with CSS. Add a rule to your CSS that targets Tooltip. Here's an example:

    tooltip02.pyOutput (before hover)Output (after hover) tooltip02.py
    from textual.app import App, ComposeResult\nfrom textual.widgets import Button\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\n\n\nclass TooltipApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Tooltip {\n        padding: 2 4;\n        background: $primary;\n        color: auto 90%;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Button(\"Click me\", variant=\"success\")\n\n    def on_mount(self) -> None:\n        self.query_one(Button).tooltip = TEXT\n\n\nif __name__ == \"__main__\":\n    app = TooltipApp()\n    app.run()\n

    TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    TooltipApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    "},{"location":"guide/widgets/#loading-indicator","title":"Loading indicator","text":"

    Widgets have a loading reactive which when set to True will temporarily replace your widget with a LoadingIndicator.

    You can use this to indicate to the user that the app is currently working on getting data, and there will be content when that data is available. Let's look at an example of this.

    loading01.pyOutput loading01.py
    from asyncio import sleep\nfrom random import randint\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass DataApp(App):\n    CSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n    }\n    DataTable {\n        height: 1fr;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n        yield DataTable()\n        yield DataTable()\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        for data_table in self.query(DataTable):\n            data_table.loading = True  # (1)!\n            self.load_data(data_table)\n\n    @work\n    async def load_data(self, data_table: DataTable) -> None:\n        await sleep(randint(2, 10))  # (2)!\n        data_table.add_columns(*ROWS[0])\n        data_table.add_rows(ROWS[1:])\n        data_table.loading = False  # (3)!\n\n\nif __name__ == \"__main__\":\n    app = DataApp()\n    app.run()\n
    1. Shows the loading indicator in place of the data table.
    2. Insert a random sleep to simulate a network request.
    3. Show the new data.

    DataApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    In this example we have four DataTable widgets, which we put into a loading state by setting the widget's loading property to True. This will temporarily replace the widget with a loading indicator animation. When the (simulated) data has been retrieved, we reset the loading property to show the new data.

    Tip

    See the guide on Workers if you want to know more about the @work decorator.

    "},{"location":"guide/widgets/#line-api","title":"Line API","text":"

    A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the line API.

    Note

    The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin DataTable which can handle thousands or even millions of rows.

    "},{"location":"guide/widgets/#render-line-method","title":"Render Line method","text":"

    To build a widget with the line API, implement a render_line method rather than a render method. The render_line method takes a single integer argument y which is an offset from the top of the widget, and should return a Strip object containing that line's content. Textual will call this method as required to get content for every row of characters in the widget.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV/2Vlx1MDAwNe109zxTtXVcdTAwMGJIIISER4BcdTAwMTBya4tcdTAwMTK2bFx1MDAxNORcdTAwMDe2eG7lv99cdTAwMWWHWPJDxjY2ce5d71x1MDAwNowk2+OZc7pPP0Z/v1haWk7vmtHyq6Xl6LZcdTAwMTQmcblcdTAwMTXeLL/0x6+jVjtu1PlcdTAwMTR2/m43rlqlzpXnadpsv/rjj1rYuojSZlx1MDAxMpai4DpuX4VJO70qx42g1Kj9XHUwMDExp1Gt/W//cyesRX82XHUwMDFitXLaXG6yXHUwMDBmWYnKcdpoff+sKIlqUT1t87v/h/9eWvq78zM3ulZUSsN6NYk6L+icylx1MDAwNqjR9Vx1MDAxZt1p1DuDtWhJkJa6e0Hcfs1cdTAwMWaXRmU+W+EhR9lcdTAwMTl/aFndXHUwMDFlnVx1MDAxY16fNnfM0d398cmhkqdlzD61XHUwMDEyJ8lBepd8n4mwdH7Vyo2pnbZcdTAwMWFcdTAwMTfRcVxcTs/5vOw73n1du8GTkL2q1biqntejtv/+0D3aaIalOL3zX0J0XHUwMDBmfp+DV0vZkVv+a0VcdTAwMDVOklx1MDAwMuuUMMZcdJd9sn89KFxurFJGgUGhrXR941pvJLxcdTAwMTI8rt9UhcolmY3sLCxdVHl49XL3mrRcdTAwMTXW282wxeuVXXfz8I3JUICCXHUwMDAwVffUeVx1MDAxNFfPU1x1MDAwZiPUgVLOKtLKOevQZMOIOstcdTAwMDFaXHUwMDFhRONM9u38hze3ylx1MDAxZGT8lZ+wevlhwupXSZKN159404+mPKJyK920Z9VduFx1MDAxMVxmiFx1MDAxOGpn764qm4er3e/UXHUwMDAzv7DVatwsd898e3iWjeiqWVx1MDAwZdOHL2GURGLgWYvd80lcXL/oXHUwMDFmbNIoXWQw7Fx1MDAxY/32clxu+IOAQvzzSljhQMmx8Z9cXLy7PD1vbev9N3uJhlx1MDAwYrmLd1x1MDAxYlx1MDAwNfgvtVx1MDAxYe32ynmYls6LOIAz4lx1MDAwMIhHSeBE4LRgiFx1MDAxYqFcdTAwMDXwXHUwMDFh9JDAuMA5XHUwMDA3vCokrdVAhSRcdTAwMTCdx/QkUFJcdTAwMDaakCRcdFx1MDAwNVx1MDAwMEOoQFpcdTAwMDRcdTAwMTaYplKTpy3qXHUwMDAxKpBTXGYkUnZmVFx1MDAxOFx1MDAwMVaDRln9LGA1aFx1MDAwYrHqnCG2XGZkx1x1MDAwNqvaxvVSZecmxNXjk1x1MDAxYqrCadIuMtZ9gPt5MIWA0JA2glx1MDAxNFx0hFx1MDAwMZhK6yRcdTAwMWFUxlhw84QpXHUwMDA1XHUwMDAytEKrXGaw0bWDOEVcdTAwMWJoLZgvWqAmxvQgTlx1MDAxZCn2KFx1MDAxYWZnslx1MDAxZsOpmVx1MDAxNU6jJImb7eGKQkNcdTAwMTFKnbcsykpcdTAwMWNcdTAwMWKk+1uV6+279t37xsV++nqrtd3ePL+cXHUwMDA2pPB8IPUwJJBsw7RcdTAwMDX2IL0gtS5QhtFAiEJYJfqFzkQg/a1cdTAwMTIqVDhcYlCgQFxuROU08i822nJcdTAwMTChgIHSWjJcbkEgXHUwMDExylx1MDAwMVEh2bxaySrwf1xuoCbnVvpcdTAwMDCK1pFGY8dcdTAwMDcolurV++Rz+aB+bj/o9kH49cRcdTAwMTS5/EVcdTAwMDGo4oVcdTAwMTdCXHUwMDFha0AjsLPsXHUwMDA1qFxyXHUwMDE4XHUwMDExvFx1MDAxNk4oyVx1MDAwMNZPXHUwMDA06JlcdTAwMTBqXlx1MDAwMCW2v0Yw1Z5ccqD2OVx1MDAwMKpcdTAwMGI1qXKs0YhyjHxcZqDHe1x1MDAxN/d7n87XXHUwMDEy8UnvbO2+PvhcdTAwMWNcdTAwMDItOEBRXHUwMDA20kipXHUwMDE0aFx1MDAwMM1cdTAwMTBQvVxiNYFcdTAwMTGCueqIp4qVwJNcdTAwMTBcbnjGmnZeXGLl8TlhrNC/XHUwMDFlQkdqUetcbr08XHUwMDAwSZBKuPFBWj7Y3CuX3lx1MDAxZlx1MDAxZX01N1dcdTAwMWZut1Vauf70XFwgXHUwMDE1U4FUXHUwMDA0QILYO3JULpSPm7BcdTAwMDekoFxcXHUwMDAw0jlEXHUwMDA2MVx1MDAxOYbIk1BqnFx1MDAxMlx1MDAxNcQhrt5cdTAwMDdEikNcdTAwMDNyUqNcdTAwMWOePtBcdTAwMWO/SZSKjFx1MDAxNPwzXHUwMDA35Vx1MDAxZoZUasPhnZ5cdTAwMDSmWVrgXHUwMDA3ZOjhyLdi9HZfMySp8PHy+uaotH178WXjrb7/fLe+d4234yVcdTAwMTVejnrfuSYrXGZcdTAwMWJkSbPiXFxcdTAwMWHdpsPohrYw9EMjrVx1MDAwNDOBSzhxXHUwMDFm31x1MDAxZidcdTAwMDdvjqp2rXK1trL1QZVcdTAwMGWnS9M9o1Mgli0sSViyoPWhXHUwMDE39NFcclx1MDAwMz5swElcdTAwMDFI4Pqj0tlcdTAwMDV/mMuOZFx1MDAxNFP9lGLyK+/DZmj650SeR0HOVoVtuppcdTAwMDDkXHUwMDE5mFx1MDAxYfX0IL7vXHUwMDAwVfRcdTAwMWPdXGJrcXLXg4dcdTAwMGX6X3Vmulx1MDAxYaVcdTAwMDFPfzlqnfKnRb/f/Sn+lV+yduRcdTAwMGb7V9uel68mcdVcdTAwMTNmOYkqvUxK41KYdE+njWZ2tsTDXHT57Vpb5f6v1WjF1bhcdTAwMWUmh49cZm0qVlNxqFxmXHUwMDFjXHUwMDBikdMwfvJRXHUwMDFklsT2znFsWnt2o11cdTAwMGJ33ea7rV+C1Up4SrNzXCKle4NlNq1cdTAwMDEosFx1MDAxNkmwi2NcdTAwMTU2P1ZcdTAwMGZLNlx1MDAwZbKa1Vx1MDAxY0p2o5l5mSepL0vr7urg8qDkbPJ2O6xcXGxdfjibXHUwMDE5qVnTimzZflxuqWFxSVxyU5JajojehHLkI5qxSZ2YKt6n+9H1p9219fhzJV7Zw5tfgNQqQMGKmONcdTAwMTFcdTAwMTamprekhopcdTAwMDJhQLP168R4pm9gs/TUQ2K2IZ6anOPh2GdcInWj9CVsnVTfXHUwMDFlxXs7d+5g/8Jd7u/OjtSan/1cXFLj4pJcdTAwMWFcdTAwMWYh9fdcdFx1MDAxZsJqsLk0Wb+vJiU5XG5cdTAwMTg/aThaqy0qq1lWM29cdTAwMWRcdTAwMWFAzf6YSd6rwDWfVtrn45RcdTAwMDUgnJ9cdTAwMDJcdTAwMDewgVWGPOJRO8rlKLPUXGZcdTAwMDRoXHUwMDA1x2eWQ3Difznp8KNkzr5cdTAwMWOJ9CS0XHUwMDFmXGZ68eHIiKB3XHUwMDE0X0lLmspcdLfTsJWuxfVyXFyv9lx1MDAwZeyhJWRrjGCvw/DSlVx1MDAxZuWKXGKUlZJNtjI8JG0zseVnJmzyNTqwqKzzVXOSzFxioVx1MDAwNr480+3xQX1U5eZXiCvq6HiXPtRcdTAwMGWqq9VwtWBQXHUwMDA2wPB/1pLjNVdcdTAwMDNjXHUwMDAyXG5cYqWUwvjAz6EzXHUwMDAzY0rCdrreqNXilOd+r1x1MDAxMdfT/jnuTOaqJ/95XHUwMDE0XHUwMDBlmFx1MDAxNv5O+XP9VqLp37HX7GfPljJcdTAwMWF1/ug+/+vl0KuLsf39bD+qs/d7kf89uYFcdTAwMTNS9Fx1MDAxZv5h4CRcdTAwMThfUp+guDxauC6uhbNcdTAwMDFLe9ZcdTAwMDO+PFwiOf7qXHUwMDE1LiBcdTAwMDOn0WhpNIl8d8LMXHJcdTAwMWPKwFdg0DphhVOZQ8/6IJitvCp8gXS+ijOsJUghaWEmalx0mrl94zHgJL08k9q30WFvzpSIXHUwMDAwhFx1MDAwNuGs9Wl9XHUwMDBlrVx1MDAxNeSu+m5LePk77S+KULMxXHUwMDE5/OpjWbeTnd23qzWx6u7bZ7T1+vBetdebw4eEmlx1MDAxOExaW7Ik+HPNwJBAXHUwMDA0JFx1MDAxNS8w+1XprFx1MDAwNPdrm7dcImT7x8ogqGdm3vKxwEDzXGbrXHUwMDE29M1EY9u30Vx1MDAxYX5x7Vx1MDAxYrJEQ1x1MDAwNGRWSqFtb1x1MDAwZVx1MDAxNTXbN+NcdTAwMWJalCAgkvOLzNin+5KEVk5LadlMXHLpoFx1MDAwMVx1MDAxNVx1MDAwMFx1MDAwM1x1MDAwNSQrXHUwMDEydJZoiIWz1lxuMmaSTq+ZW7ipI64xLdzoXHUwMDFjQI85YelGzCpjkWWEM4NcdTAwMDbO16TYhnhcdTAwMDPoW+zUYMVmLFx1MDAwYrd1e5rcrVSPcUW3PsLnzXfNK7k3fEhsctF6YUasZyzpIUNcIoau0lxuoNP++ovbt0Jg+8fKIKYnNHCFWSco7lxyRI7IWDVOXHUwMDEyoI7U51x1MDAxM5m3wu7Actg+j2bZyc3YXHUwMDE2qHjWmY/ev/dFp8h+lGkhnWKZl/dcdTAwMDYzzzrl2oeyrFM21q7xXHUwMDEyXGZcdTAwMDRHM+yu6mJptkXQl6Ped751J2JdlKVTp8tmge452s1mZaWGXHUwMDFm2ayDtFx1MDAxNTd//087qvopfbnUfVx1MDAxMlx1MDAwNMFfXHUwMDA1SS3d8y7zTmo9MsKRpmNkP4fLtSn221x1MDAwZtZGPvKewH7EryVtv0nCS6rJXHUwMDBioq9cdTAwMWI3t7VcInm0IP1cdTAwMWMrXCKQaIT1qlx1MDAxM1xmWtvfXHUwMDE3XHUwMDA3Tlx1MDAwN1xuXHUwMDA0XHUwMDBiVO8/zdOa4Ofdz+FNvlx1MDAwNtLuacroiVxyXHUwMDFk8yxfkW9cdTAwMDM3z9PsxIFjYfLXi+lOz8H4RZ1K+0vdnF9cXH84PLtf+1irt+tuM1l4dqBwznI4L1x1MDAxOXhGqL6aXHUwMDBlSfa+TivB/6N4Ws/onLnB4ouv6Nm18Vx1MDAxM7gxzypcdTAwMTD5lqBcXNpnTk1JJFx1MDAwYiud2vq9RGqCRurRSZNcdTAwMDWVnDogjUL4rVNWXHSw/d1cdTAwMGJcIjDo87iW8WjNXHUwMDFjXHUwMDBinWNKTuLoz29cdTAwMTN6XHUwMDE2xTlX62+F31fzjzKciTIs7k8q7jpcdTAwMTSKpFx1MDAxNdaNnzNcdTAwMWKdNFhQhqtcdTAwMDBcdTAwMTUq0dnAxXymfrdnXHUwMDAyXHJW++SF1ajnV1x1MDAxMVx1MDAxOJPhljQoXprniSnn6sNcdTAwMThcXEr/XHUwMDEz+82X4a44Ka6N76PNV8dcdTAwMWYjOJhcdTAwMWS92fq0cXK11npT3r9d/bJn381U146i9zBh+yi9/T5O5dCXu5Umlvn9/Fx1MDAwZVgtXHUwMDEyKVx1MDAwZfeU0UZcdTAwMTdcdTAwMGLbMbb/j1x1MDAxMLaghm0hXHUwMDFk7FRija2cmSysW9Rd/lJcdTAwMTgzVWVwqk6l90yypdW9raXjTl/QXCJ0KPVcdTAwMGZpJINcdTAwMGIrW7r4XHUwMDBlXHUwMDA2KKRcdTAwMDSfa1x1MDAxZb+LePSSz3lX+FRcdTAwMTT2XHJcdTAwMTOGY0Gyxlx1MDAxN4Ogd1x1MDAxYlx1MDAwZVx1MDAwN4zOXHUwMDE3iDhe1Fx1MDAxMmGEh35cdTAwMWGBpVx1MDAwYoS0vqeRQ1x1MDAwMVx1MDAxNlx1MDAwNEPobGWgrZagJZtcdTAwMTOgXFz+6MF3I1x1MDAwN21mlvtcdTAwMDVGsk/lXHUwMDBiXHUwMDAys69ajXZcdTAwMDZLPUVwYVx00Vx1MDAxOFx1MDAxMKT81FHuqodcIjhcdTAwMDbGMZitXHUwMDAzniRp7WCPz1h1q9H7zHpcdTAwMDYloNM6gdJcdTAwMDdcdTAwMWHo01x1MDAxZYOjooC1oUFeclx1MDAwNpiU4tcuXVx1MDAxNULYP/rBm73Zi/zv6dJrVHxjXHUwMDBi35BmnZ5AhVT3bk72t1blUUlhfFgtl0qfP5ZnqkImtGDqUVx1MDAwYlx1MDAxNiAozfAxvu8kn9TsaFx1MDAxMCNcdTAwMDLpXHUwMDA0XHUwMDBiXHUwMDE0R8hRRv+wZpZbU96OknWK/OpcdTAwMDNcZuudfjS5ZslqY1xyPq2ncpFza7xcdTAwMDIzvOFLkTBXuW6HPkpcdTAwMTAvgGOvP75T39i5rq2vJPK02Tyrne5v7X51Z5s/OfKWj0rzwJBjny4sXHUwMDE4Ry5/XHUwMDAzle+0kFx1MDAwMet2sEjOXGIj5rjfb9x6rtTkd8o9z81cYizHXHUwMDAyk2w7/Sc6ntY7gSws/liw3v+Pn1x1MDAwM/uyefYubFx1MDAxZdy//Xik11x1MDAwZeMvd9vVm2e7XHUwMDFiw1TOybeFS/Y9yMFcdTAwMDSgXHUwMDEykEv6d7BcInRgWFx1MDAxM1xinlx1MDAwN806ySx0XVx1MDAxNFhnWpA0/43uI6sz/j4z8/YgUlx1MDAxNXpcdTAwMTDQYNCx3lx1MDAxON+FvK9cdTAwMWbbo1J8vH5v65XD083W12azseguhFx1MDAwMoaLUuyzfVecXHUwMDEzvVx1MDAxYlZcYjj08Dd11PyDr/rpXHUwMDBlRFx1MDAxOVL+plGz24Y20oFImKhE+I9cdTAwMDOZ2oFcdTAwMTTfY1x1MDAxNaThZfftx2MzcVVUrrd0RUNyrfZKXHUwMDFm4q9n9rqojPIsLuRRXHUwMDFh+lx1MDAxNFxmXGJGtkO/P6xv31x1MDAxOElcdTAwMGWjjXaC40q0+Z7kXHUwMDA19Fx1MDAxZlx1MDAxY4uSr3z9XFz34UvN83dcdTAwMWaFXHJhKCx6XHUwMDFkYMbXPXftjav7hjsyNqkmZXO6oe7fvVl098FoIHKotZ9vwVx1MDAxZaJcdTAwMWa4Olx1MDAxMP5mR75Q5XdcdTAwMGL99Po+XG7lt1SoXHUwMDE5llx1MDAwN0Z6XHUwMDEwjrqeelOQ/ztcdTAwMGby4iG5sFx1MDAxYzabXHUwMDA3Kc9o10LwWsXlh2nJPmf5Oo5u1obdWa/z8O/aobbnUNQxN99efPsvXHUwMDFht7uQIn0= widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])

    Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code:

    checker01.pyOutput checker01.py
    from rich.segment import Segment\nfrom rich.style import Style\n\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\n\n\nclass CheckerBoard(Widget):\n    \"\"\"Render an 8x8 checkerboard.\"\"\"\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        row_index = y // 4  # A checkerboard square consists of 4 rows\n\n        if row_index >= 8:  # Generate blank lines when we reach the end\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2  # Used to alternate the starting square on each row\n\n        white = Style.parse(\"on white\")  # Get a style object for a white background\n        black = Style.parse(\"on black\")  # Get a style object for a black background\n\n        # Generate a list of segments with alternating black and white space characters\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(8)\n        ]\n        strip = Strip(segments, 8 * 8)\n        return strip\n\n\nclass BoardApp(App):\n    \"\"\"A simple app to show our widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard()\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp

    The render_line method above calculates a Strip for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard.

    You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those.

    "},{"location":"guide/widgets/#segment-and-style","title":"Segment and Style","text":"

    A Segment is a class borrowed from the Rich project. It is small object (actually a named tuple) which bundles a string to be displayed and a Style which tells Textual how the text should look (color, bold, italic etc).

    Let's look at a simple segment which would produce the text \"Hello, World!\" in bold.

    greeting = Segment(\"Hello, World!\", Style(bold=True))\n

    This would create the following object:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1aW1PbRlx1MDAxOH3nVzDuSztcdTAwMTM2e790ptOhJFx1MDAxNEIuUEJoaTuMsNa2gixcdTAwMTlJ5pJO/nu/XHUwMDE1RpIl7CBjUqd+XHUwMDAwe3el/Xb3nPNdpH/W1tc72c3Idn5cXO/Y665cdTAwMTdcdTAwMDZ+4l11nrn2S5ukQVx1MDAxY0FcdTAwMTfNf6fxOOnmI1x1MDAwN1k2Sn98/nzoJec2XHUwMDFihV7XossgXHUwMDFke2Gajf0gRt14+DzI7DD92f1961xy7U+jeOhnXHQqJ9mwfpDFye1cXDa0Q1x1MDAxYmUp3P1P+L2+/k/+t2JdYruZXHUwMDE39UObX5B3lVx1MDAwNirJ661v4yg3lkhFhGRcdTAwMWHLYkSQvoD5MutDd1x1MDAwZmy2ZY9r6vAjuvlhK+7ffPQvtlx1MDAwZcJNSo7CP8ppe0FcdTAwMThcdTAwMWVmN+HtVnjdwTipXHUwMDE4lWZJfG6PXHUwMDAzP1x1MDAxYrjZa+3Fdb6XXHUwMDBlwICiO4nH/UFkU7dcdTAwMDO4aI1HXjfIbtyNcNl6u1xy1XHX7pBcZkZSca41Zsatueh11zNKXHUwMDExx0ZLyZRcdTAwMTCMypphW3FcYmdcdTAwMDGGfYfzT2nZmdc974N5kV+MyVx1MDAxMi9KR15cdTAwMDInVo67miyZXHUwMDBiiqQglCqMXHKunszAXHUwMDA2/UHmjosgho1SynDMsJCiNMbmp0JcZnN2XHUwMDEyo4tcdTAwMWVnwmjXz1x1MDAxMfJ3ddtcIn+ybXeQKUHDJi2fy8W48S/rYKtcdTAwMDKugoN3e0dXxmNDtn/iXHUwMDFk7uyeblx1MDAxZqTvVLHgKXR6SVx1MDAxMl91ip7Pk2+loeOR791cIlx1MDAwZVx1MDAwMCk4XHUwMDExWlFDTdFcdTAwMWZcdTAwMDbROXRG4zAs2+LueVx00rXKStqxg1a2scZcdTAwMGVcdTAwMDBcdTAwMGLRilx1MDAxOf5gclxcnrw7vd5S9rV93dvjsYjt/lx0WYxcdTAwMWN0XHUwMDE2OdJcdTAwMTgk4n5ukFx1MDAwNblhkFwiXFxzXHUwMDAw1TQtXHUwMDE4QVhSMZNcclpSa7qLs4FiiaQhXHUwMDFjJuHGfVSTXHUwMDBlgiFtpm2744HEmlxuzivn80Q8mIdUXHUwMDAxu8SWhdTMXmf3gnQmRlx1MDAwNTVcZnSLPlxcwI9Aen57b7btx4R9XHUwMDE0Nt7ZXHUwMDBlg8GSXHUwMDA1fOlcdTAwMThlRFwirakmtFx1MDAwNlH+dFpNKtJbwJHyOlxmXHKmXHUwMDA0XHUwMDBiptqgcFxuXHUwMDFmrWT3evx7cPkq+nBp9/ax2Lu+Or5cdTAwMWPsLUl2OWfaXHUwMDE40lx1MDAwMswlauIoO1xmPtmc1FOt294wXGLzoyqac5SDgX91dmxcdTAwMTjGz9aP4yT0/+pUjyq1MHvtdu66zTDoO0Z0QtubpkpcdTAwMTZA8FR0Z/Go7O2CXHUwMDFkXHUwMDFl3C7Z9evriZOgXHUwMDFmRF74fpZNi3tcdTAwMTbGVL31jrWGXHUwMDEwIK3hXHUwMDBmZ+3u3ubbbSrVxYuD991cdTAwMWXTn3x/J5jB2lx1MDAxYfv+K79cIihGVIBCmqZjwVxiT7XWucsk73XVIzxcdTAwMGJhSNx5ldaeRTpBxZqV6/m/XHUwMDA1WMYwgkvFelwit0VmM4AoyjGRpEVwdfM2ftU/O9u/XHUwMDEx4uJ0g1x1MDAxZI9e71x1MDAxZmyuuuNcdTAwMTJMI6yVXHUwMDEyhlPnKNg0XHUwMDExuECSc8HInCjr0X5Mmyb4m35MSaG5Ulx1MDAxNU16Sj/2q/fJo3tcdTAwMDO6uSuOduTxsWTnLy+Wlj5cdTAwMTjNOG2B7sf5sVx1MDAxYz3fn8Wh/9P7ZGx/WFx1MDAwNT/WsGmxuLNidZ3AnEHKXHUwMDAwaWhcdTAwMTmafonAL0/UwcvRfnK+8bt9dbQpe1x1MDAxYofh6apcdTAwMTOYUYYoJNzwMVxuXHUwMDEy93K57nouOVx1MDAwMvpSXHUwMDE3PHHYjbp3XVwii+k9Low2XFyXkka6QK6N61rNaFRcdTAwMTBcdTAwMTBF9lQsJlNsdFx1MDAwNvZcdTAwMTNrsyDqo2kyVChcXIH616DwtEGL8Vx1MDAxN8/kL/gjXHR7rMpcdTAwMDFfou9V/8Ppm1x1MDAxNyPpXHUwMDFmvtl68UZEoThcdTAwMWOfrDp9hYJA0HBiZO6AWZ2+XG5RIDV0XHUwMDAwezWresVl87dCyTn8hWReSY1L2H+zTphcdTAwMTCBK1x1MDAxOfRXo2+aY2mV+Htr0VxcXHUwMDAy327vfUlkpVxmWvfAWlx1MDAwYlx1MDAwMVx1MDAxOU5cdTAwMGJcdTAwMGY8X69XlMJMXHUwMDBiJFx1MDAwNGR04Fx1MDAwZaRcdTAwMDF/PFx1MDAxZENzSZCmmFNtnthcdTAwMDVcdTAwMWKkQElcXKFEaqOwvqc8pDRSICXmLlx1MDAxY6hcdTAwMThzV7yXcFx1MDAwM6ZcdTAwMWVXu6eTlsVqlos71jTzkuyXIPJcdTAwMDHW04ZNnlHtPiDQy7ncXHUwMDFkOys3MMJcXFx1MDAxMVc9g3CSUWp4+WjG7Y03cqtFkuWlv8aqbeR/2Zr5XHUwMDA1z4o1YFxmwVx1MDAwNsPeMEyoNIRy1TCGMMjn8pJGw5rQS7OteDhcZjLY7v04iLL6tub7t+m4PrBeQ0BgNdW+uiiM3Fx1MDAxZKc1vfy2XpIm/1F8//vZvaNnYtl9Nlx1MDAxYTAub7dW/d9azqiaWclcdTAwMDYh4EJcYqVcdTAwMWUuZ/P914rKmWRcdTAwMWNcdTAwMTmqMVMu6IDMoaFmsFx1MDAwNdJhzEUkrGbX8tRcZuJcIsyNUkZcdTAwMGLIb4SsJP6lnCmEXHUwMDFkXHUwMDE1INOrXHUwMDE5M1EzJjXRXHUwMDFjyzZFg2XL2eLZ/lx1MDAwM+VsfuBblzNcdTAwMDaUYiBoXHUwMDEwVzqK8cq4W1x1MDAwNVx1MDAxMYgzdr+CPEjP5tfBpvWMXG7KNVx1MDAwMVx1MDAwNitOsKaGNawhXHUwMDAykVx1MDAxOer6LenZxmw45911JLdcdTAwMTS0WVx0lmGi3lokWFxmpuK0zZO5M9zt9a8hXHUwMDFhXHUwMDFkXHUwMDFmXnw8+qN38mv8epFcdTAwMWH/XCJitth7XHUwMDE1Tq5cdTAwMDTPdUxgXHUwMDAzwVlZpHA3oJQh8KJGXHUwMDEy6TA/O7tcdTAwMDLn7/H5YvZdr9drqph6UFWEQOKHtWStdGrxvOqJS/dSV6pQXyuvWqWMasFcXIpSQurNRfRcdTAwMDFbXHUwMDBiQCUtkqn5p7ySj+S4XHUwMDExiCqmlVCUS1x1MDAxMMKSkTldhUBaQSxuXHUwMDA0eHZiKq/GLI2wXHUwMDEwP3NcZiqtJOdcdTAwMWFCoEq4Vz6ZI65mg4VDPDZcdTAwMTJX9GzygFx1MDAwZbSVske+XHUwMDAw9aioQ2JcdTAwMTeutnlFqW3UMd9cdTAwMWJMuXnKMVx1MDAwM99GKTbCbUwz5lBIuUhAaYWpXHUwMDE0RuNcdTAwMDVjj/lv/9ViXHUwMDBmLMDlau5cdTAwMWVwSaFpwyhcdTAwMDKBMNPgrImGMFx1MDAxMoY0n5h+SyHIbGS7T1x1MDAwM9OzXCKQtclcdTAwMDRcdTAwMWRvNDrMXHUwMDAwdMVxXHUwMDAwylx1MDAwM39cIuDlKjuXgb365X76OVx1MDAwNq5N9tNcdJLNmfB57fO/MTl+mCJ9 \"Hello, World\"Style(bold=True)greeting.textgreeting.stylegreeting

    Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments,

    "},{"location":"guide/widgets/#strips","title":"Strips","text":"

    A Strip is a container for a number of segments covering a single line (or row) in the Widget. A Strip will contain at least one segment, but often many more.

    A Strip is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text \"Hello, World!\", but with the second word in bold:

    segments = [\n    Segment(\"Hello, \"),\n    Segment(\"World\", Style(bold=True)),\n    Segment(\"!\")\n]\nstrip = Strip(segments)\n

    The first and third Segment omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text \"World\". If this were part of a widget it would produce the text: Hello, World!

    The Strip constructor has an optional second parameter, which should be the cell length of the strip. The strip above has a length of 13, so we could have constructed it like this:

    strip = Strip(segments, 13)\n

    Note that the cell length parameter is not the total number of characters in the string. It is the number of terminal \"cells\". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.

    "},{"location":"guide/widgets/#component-classes","title":"Component classes","text":"

    When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining component classes. Component classes are associated with a widget by defining a COMPONENT_CLASSES class variable which should be a set of strings containing CSS class names.

    In the checkerboard example above we hard-coded the color of the squares to \"white\" and \"black\". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the \"white\" squares and one for the \"dark\" squares. This will allow us to change the colors with CSS.

    The following example replaces our hard-coded colors with component classes.

    checker02.pyOutput checker02.py
    from rich.segment import Segment\n\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\n\n\nclass CheckerBoard(Widget):\n    \"\"\"Render an 8x8 checkerboard.\"\"\"\n\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        row_index = y // 4  # four lines per row\n\n        if row_index >= 8:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(8)\n        ]\n        strip = Strip(segments, 8 * 8)\n        return strip\n\n\nclass BoardApp(App):\n    \"\"\"A simple app to show our widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard()\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp

    The COMPONENT_CLASSES class variable above adds two class names: checkerboard--white-square and checkerboard--black-square. These are set in the DEFAULT_CSS but can modified in the app's CSS class variable or external CSS.

    Tip

    Component classes typically begin with the name of the widget followed by two hyphens. This is a convention to avoid potential name clashes.

    The render_line method calls get_component_rich_style to get Style objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.

    "},{"location":"guide/widgets/#scrolling","title":"Scrolling","text":"

    A Line API widget can be made to scroll by extending the ScrollView class (rather than Widget). The ScrollView class will do most of the work, but we will need to manage the following details:

    1. The ScrollView class requires a virtual size, which is the size of the scrollable content and should be set via the virtual_size property. If this is larger than the widget then Textual will add scrollbars.
    2. We need to update the render_line method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.

    Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.

    checker03.pyOutput checker03.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\n\nfrom rich.segment import Segment\n\n\nclass CheckerBoard(ScrollView):\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\n\n    def __init__(self, board_size: int) -> None:\n        super().__init__()\n        self.board_size = board_size\n        # Each square is 4 rows and 8 columns\n        self.virtual_size = Size(board_size * 8, board_size * 4)\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        scroll_x, scroll_y = self.scroll_offset  # The current scroll position\n        y += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\n        row_index = y // 4  # four lines per row\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n\n        if row_index >= self.board_size:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        segments = [\n            Segment(\" \" * 8, black if (column + is_odd) % 2 else white)\n            for column in range(self.board_size)\n        ]\n        strip = Strip(segments, self.board_size * 8)\n        # Crop the strip so that is covers the visible area\n        strip = strip.crop(scroll_x, scroll_x + self.size.width)\n        return strip\n\n\nclass BoardApp(App):\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard(100)\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp \u2585\u2585 \u258b

    The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the virtual_size attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.

    The render_line method gets the scroll offset which is an Offset containing the current position of the scrollbars. We add scroll_offset.y to the y argument because y is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.

    We also need to compensate for the position of the horizontal scrollbar. This is done in the call to strip.crop which crops the strip to the visible area between scroll_x and scroll_x + self.size.width.

    Tip

    Strip objects are immutable, so methods will return a new Strip rather than modifying the original.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXNcIkmS/d6/QlbzcZuccI97zMbWdKFcdTAwMTPd6GB3TMYpkFKAOCSktv7v645UXCI5XHUwMDEyoYvKYtVtqiqSI4h87v6eh4fHX38sLf3oPDbLP/619KPcK+bDWqmVf/jxJz9+X261a406XcL+v9uNbqvYf2a102m2//XPf97mWzflTjPMXHUwMDE3y8F9rd3Nh+1Ot1RrXHUwMDA0xcbtP2ud8m37v/n3Xv62/O9m47bUaVx1MDAwNYNcdTAwMGZJlUu1TqP1/FnlsHxbrnfa9O7/Q/9eWvqr/zsyula52MnXr8Jy/1x1MDAwNf1Lg1x1MDAwMVx1MDAxYWVHXHUwMDFm3WvU+4NcdTAwMDVcdTAwMTRotdBavj6j1l6jz+uUS3S5QmMuXHUwMDBmrvBDP453K1AvX/dcdTAwMGWLK6nWib/e1M3LjcHHVmpheNx5XGafp1wiX6x2W5FBtTutxk35rFbqVPnTR1x1MDAxZX99XbtBszB4VavRvarWy+320GtcdTAwMWHNfLHWeaTHpHh98HlcdTAwMTL+tTR4pEf/SjlcZjRcdTAwMDJ4kFx1MDAwZZXE14v8auVcXKC0tVJIROE1yJFhrTZCulx1MDAxMzSsf4j+z2BghXzx5opGVy9ccp6D6FxuZTN4zsPLlzUmXHUwMDEwylqtUGlnhFevz6iWa1fVXHUwMDBlPcVi4KQ2wlx1MDAxOIHCOTdcdTAwMThnu9y/XHUwMDFmXGJSKbruXHUwMDA3N5Q/vrlV6mPjP9FcdTAwMTmrl15mrN5ccsPBiPnCelx1MDAwNE+D13SbpfzzbVx1MDAwN2O1Rk9DoLl5vVx1MDAxZdbqN6NvXHUwMDE3Noo3XHUwMDAzpPRcdTAwMWb9+89cdTAwMGZAlO5KXHUwMDFjRK1cdTAwMDaNXHUwMDAy3OxcYl3LVm6ue8t7d1x1MDAxOXl2eFxcObnPZdeWXHUwMDEzjlBtXHUwMDAy0MpcdTAwMWGp0ErrnDfjXHUwMDE4ldJrXHRcdTAwMDRTacEkXHUwMDE2ozR85b0wesEgaoWLgyhISSZcdORDZsZo+WRzm1x1MDAwNl8olNTRQWbL7excdTAwMWPcqIRjNFx1MDAwNTJcdTAwMTDgnfZgwVx1MDAxOedgXHUwMDE4pNrIQFx1MDAxOSfoXHUwMDEyoFOj40pcdTAwMGVEQUvp6D9YOIjqOIhaa5SQxsLMXGI93y/s7VxcaFdoXHUwMDFjXHUwMDFj3ubP7zeJ37R/LUJBvOlGVWC1oWl3XHUwMDA2vFFgh1x1MDAwMGqkXHUwMDBmiFx1MDAwM3hPXHUwMDFlSmilMbFcYkWwVtFQXHUwMDE3XHUwMDBloVx1MDAxMEtFUSjiNqD97IH+NpXLy3RxOXucq96U16pcdTAwMDfrXHUwMDFiISTdiTpcYoiFKidccjqg71xmw1x1MDAxMFVcIiBcdTAwMGaqtFx1MDAwMkVE1erEQlx1MDAxNCyRXHUwMDE0TTFg0bio9bFcXFx1MDAxNFx1MDAxY01cdTAwMTdFNz27XHUwMDFi9TZvdNi4vu5cdTAwMTazcFx1MDAxOXZLjX27l3CMXHUwMDAy6sBcdTAwMWKhKcxcdTAwMTNcdTAwMDR8RFx1MDAxMD2HeU1cXNRcYutQ9mNKciHafy25lPm5UT9cdTAwMWa5JHxcdTAwMWNEtfDOXHUwMDAy+plcdTAwMDHaetquLtv1PVxym5uynFp7Slx1MDAxZlRPk1x1MDAxZefpzlKgJ1x1MDAwNmdR05/aXHJLesJtgI6UvDGoPGKSqahw4KxcdTAwMTBcdTAwMGJcdTAwMDdRXHUwMDE5n3RykmKbfIdcdTAwMTNdllx1MDAwNd+53Xpyjerm8Xr2XGbyp/WkK/pcdTAwMTRgIDmvRLLeoSBHaUYwqomMXG5E68lFXHUwMDE5n9ysXHUwMDEzgDWk6oWaX6SfXHUwMDBmRq0wcVx1MDAxOPXgPOiogHhcdTAwMGKjK141fKdzs1Jo3+CtKUH7ot5MOEbBXHUwMDEzXHUwMDA2lVx1MDAwNVx1MDAwMUpcdTAwMDLFe1x1MDAxY470ylx1MDAxOYKGoD+EQYr3oJKLUUnwcdZEXHUwMDAy42JglD4pXHUwMDBlo8zMlNJmdozuhL3qWlXmMlx1MDAxZFFcdTAwMGXXzcNj2OydJFx1MDAxY6NSyoCYptBcdTAwMTTKhTM4mrwniFxuY+mOWCFRJDgxSjZGMUBoOb/M6JzcqME4iFwiKE30Rjo9M0b1zXpYKFXu08f3WpRy5+FlfT1MOEbRqEA68j9cdTAwMWWU0TKShntcdTAwMTZMXHUwMDEwOMKuQk5MYYKdKPlcdTAwMTJA4qNcdTAwMGKHUFx1MDAxZJu7d2hQ0q2ZPeu0eqMuOnBdusruXHUwMDE1W6dcdTAwMGYtma3m80lcdTAwMTdM7EWV8kQzXHRcdTAwMDFcdTAwMDCRtYyfWSdA4ywvjoIzXHSGqJVcdTAwMTL418JBVMk4iKKj2dL+XHUwMDFkq0v6+mJze9dm95dVuYHb+6enzZxNOkTBm4Cw4STxXHUwMDFhXHUwMDAxUo5hXHUwMDE0XHUwMDAzLSjWkGJcImbuklx1MDAwYlGyMbpjOMdF+jlRUVx1MDAxZFx1MDAwYlFtke5cdTAwMWH42SU9bi7bo3C7lrtfbV5f7eNtRVxc51x1MDAxMlx1MDAxZeaVkIEha1x1MDAxNN4rXHUwMDBmVo2GeVwiXHUwMDAxSMhcdTAwMTDSeUy0oDfaWWW1XTQn6lxcbF6UvIvxRkaXSN+C6F7n8r6mbnqnzZW73HLnMXN8VF3/XHUwMDFkIGqEJS6qnZMjqXvOOVx1MDAxMVd1gqhcdTAwMDCr/eSKJS6FsVx1MDAxNtTCIVTGMlFcdTAwMDM0a1rI2X3oxe7DRqHaq4Rdd7ZV92erjfr+RdLDPNHMwFiHJJPAOI3KjkDUXHUwMDA2XHUwMDAyXHUwMDFk31x1MDAxOGe4jiixXHUwMDEwReEleVHQi5ZysjLWiyryoUJqNbtcdTAwMTNcci+ecvumqFx1MDAwZm/NQ72zv3N73To/TLhcdTAwMTNNOZJD9D3BSclcdJtcdTAwMTGEelx1MDAxMVgwSoNcdTAwMDaLTiVXLCktkWJeRFksXHUwMDA2QJ1cdTAwMTdxXHUwMDAwXHUwMDA1Vlx1MDAxMHTv9OxcYr16KKZPa21wl9nH7VpVmePc+i9Ois5Q6GRJXGZJ+qaEQZr3KD6eMVxugdJkrFZcdTAwMDOp/YiLSlx1MDAxYUbpXCLxXHUwMDE05Vx1MDAxNm3908ZjlL+wgWhZxZvVou2KKp6c7Oid5mrT5DLd6nphM+lOXHUwMDE0REA6wztcdTAwMDOoud4uUpPAb+BcYj2kokjqk2Am8CS3jMRcdTAwMTlnhV84JlxusVFcdTAwMWWFXHUwMDE2Xoj3rCx5XHUwMDExVjvdwoYt75ZqXCJzXuymdlx1MDAxYlx0Ryj5UGk8ek1cXFR7gcNy3itcdTAwMTF4hSRDXHUwMDE0XG6Z4Fx1MDAxMlx1MDAxMpLxxEXkwiXtnYivXHUwMDE1JXySdphcdTAwMWSehUZcdTAwMTWuWts7t08rXHUwMDA3t1f3ze398/VMwuGZsi6w1pIs1I6IJrnRXHUwMDExfMpAopTOoqbJwORWiqIgLi1cdTAwMTDEwvFQXHUwMDFiW2+PLJSMtbPT0OPMRerk5nxv67540d3aL4dcdTAwMGZcdTAwMGb791x0hyigXHJcdTAwMTR7SFCS5LBcdTAwMWGN8Fx1MDAwNGAgnU86RHlcdTAwMGLJLbe3aLVfQFx1MDAxN+ohNtukpLXo4Vx1MDAxZEp+fcs9XFyd7tQz9+XD7PLKSemyW096OpRcdTAwMDHKy1x1MDAxMp5cdTAwMWOQ5FxyIUNcdTAwMDAl7qlcdTAwMDMhNTlZrZXxNsGLSob/035cdTAwMDG1fHwps1x1MDAwNE9cIvZcdTAwMWRL8/dcdTAwMWJcdTAwMGad86Otq8xarry6fX2xXazmz1x1MDAxM1x1MDAwZdFcdTAwMTS5UMF1MsJpY6xcdTAwMWYpZSaM2sAp+lx1MDAxMUT0SEol14tcdTAwMDJdsDSChcuHmtgw7zQ4XHUwMDE33Y79pk7avYHbTmpzt3l3XHUwMDEwbuyk6j19eZD0ZFx1MDAxM3jL2+eFQCSkRpcwnnNNXHUwMDE4KND9XHJ3hGKdXFylXHUwMDA0QCYk1OIlRH188VxiOO2dXHUwMDE3XHUwMDA2Zlx1MDAwZvS5Wlx1MDAxNcqNVLpWz2RrOrO50fBcdTAwMGa/WMvPUuBkXHUwMDAyQqDXllx1MDAxM0p+dFWJMCqJXHIxepBcdTAwMTdtXHUwMDEyi1FjUJOkc4vmRJ2MdaIgOIXt5DtcbpzOT1x1MDAwZotccn9YujqrXHUwMDFkZU+0bj21Vs5cdTAwMTJcdTAwMWXo0fDKJil2pYiWXHUwMDEy3Vx1MDAxY07ZO7qsSMqT4JdcdTAwMWGT7EXRWuJcIkYvmlpyJjbhpFhcdTAwMWPCe7YsZc+3m1x1MDAxYrWtjaNMa2tXn3SOz07bSW+TI6VcdTAwMGVcdTAwMWNcYqKhxEhp1ofVkldcdTAwMTA4a/t1I+RpXHUwMDEznFx1MDAxMVx1MDAwNcnKwZk59iCZV1x1MDAwNV5spT2QNDBWKJw9zvuV8/rpffWyU8BcdTAwMTW3WXkqrJVcdTAwMWbuXHUwMDEyXHUwMDBlUfAuIDFvhffaaFx1MDAwMGlGMKpcdTAwMDKKn2iIqYJEmVxcQe/J0Kywi0dFMX7fp1x1MDAxMZr3jNvZNydcdTAwMWbs7dhuSp7ByeN1/cGmsVPYLCdcdTAwMWOiSlx1MDAxMDxIXHTT/UUhhVx1MDAxZNlcckJyMTCaVD1xVMNro8mFqDZcbr3Ri1bH7OPT9oCO63tRzVx1MDAwZdH1Qlhv3e1cdTAwMTfSXHUwMDBltleObjZuXHUwMDFl1sKjxEOUXHUwMDAzPVFRYclcdTAwMGaBXHUwMDE4dqL9tKi3gvyr8kbK5Fx1MDAxNo9cdTAwMDCS/Vxit4iCPt6LUtBcdTAwMTDc9WB2Lrpmd+5ztfW1p/AqbNbq4DauNm5cdTAwMTNcdTAwMGVR8p+BIMmOdPspkFx1MDAwYmdHMcqro16C1nR7olQvcSBVvDuVSfOCgVSa+I54zit05Edmz91X7/Nb6V1cdTAwMTf2Llxua2m7nXdHO+W4XHUwMDFhp1x1MDAxMbBcckN0tFx1MDAxZfP1VaV8u1p+XHUwMDA3Ru0sbUVZXHRpZTn1NJq6573JXHUwMDA2eEcl31x1MDAxOTDuUyVOnVa+3m7mW4SBcZxqrVx1MDAwMmOs05ZmXurIkskrTlx0oUG/o5TWRJ9hUs8xxWu5qL5cdTAwMGWnL1x1MDAxN1x1MDAwNsCK3O+Gvdjcv8Dl5ulx+7RSa/m6yVxmmndccqEw32o1XHUwMDFlfrxe+fvPae+bOd+9XHUwMDE2+TOzVz0uioNmoVx1MDAxM+7ordne9+VvybAucDZ+M4vgLXJcdTAwMTbf0c5v+bKS71xcXHUwMDFk7Fx1MDAxZjdSVVkopo+PN1RcXD5iqnmNrj99azM/bblcdTAwMTZNXHUwMDExTyalN8yjjXRcdTAwMDFcdTAwMDV+0CwriG/Hl2HTs/LqjVx1MDAwMFCpVMatSipHI1BI4ZY+30VcdTAwMTbTX62KXCJT4HkvXHUwMDAzyU2DUX3+umHVaMlcdTAwMWJWv45FT7Wq89LpKt5V7vaq+96Yxoo5yJz8juhcdTAwMTdcdTAwMTCf6rA0o85F6/LfQr8tbu7Ui4VO53Hn3pnlp9Ku6vaSvqShdSB5r1x1MDAwNVx1MDAxMoSUVsRyhlxyQEEgyd+DI1x1MDAwNjy0pTtpXGbIglRCLdy6MFxijMcoXHRJ40HNzn+2lrP3XHUwMDBm4U1W5S5219IrWbVij4pJhyh4XHUwMDE1oNFWcl9cdTAwMGIyWTWGUFx1MDAwMVx1MDAwNlx1MDAwNXlp3tGdZI7OXHUwMDA1XHUwMDE4Xi9cdTAwMWNH91x1MDAxMYowuqbRryrRZvZUhzvdyJz27uzmyurxzr06XHUwMDBm5ermL27DNsvCMFx1MDAwNqSYvSdcdTAwMDLurbejPYFcdTAwMTVcdTAwMDaSN1x1MDAxYzpP0TvJzVm80t5TXHUwMDE0WDg3qlxcrFx1MDAxYjVWSVx1MDAxMlc4e6TvPS7fhPlwfTt31MvlTn0h3HWtxPNcXCG04jVGr7mkeyTOXHUwMDEz0SWKS1x1MDAxNJIgqpz93I7YXHUwMDE4omtcdTAwMDOtaFx1MDAxMJKbwGk1QT7CM9tGYl7cKFx1MDAwNqL7dV52XHUwMDFiXG6Dyir4ukW3aURcdTAwMTex97jTbsPxg6rD9uPBtj+utWcjulPl429EoMthWGu2J9tcdTAwMTTxxjibQsKIXHUwMDA2Zdzs4nE1tfm0dbyXlb1cdTAwMDPZvs4101x1MDAwZvthXFxcdTAwMDJxqlHNz+1cdTAwMWKiXHUwMDFlfI5cdTAwMDa3XHUwMDFmXHUwMDEz0c6Fz05fXHUwMDEzMUHJus1+cot5Ja9R47hNsXS0xtHryb+p6IlcdTAwMWSvNuUoKqDtb9Hs96yXoyZcdTAwMDVcdTAwMWVcdTAwMTVyt/D5iMdP2FSSsK+mtPXkXHLsxsvZXHUwMDE3yVx1MDAxZkXtonq/bVx1MDAwMVx1MDAxZXZ6XHUwMDE1n013u+eYbOhbctVAmDFeSEUqeVQ2qn5iXVvlUVxu87l64kq+QN7ke9BvKfJcdTAwMTn9lfnIZMBTx+f1nOZ2rO9pUrPSOzg9Or9q1zOt+onRqdst7dLJxqc3XHUwMDAxeUyJwnpBXHUwMDE4XHUwMDFjQadcZlx1MDAxY0hcdTAwMDOWa+GI9Hxcbp2AXHUwMDA15ybw8a9AJyrlXHUwMDAwo0dcdTAwMDH8Nuh8K/NcdTAwMTa79qiNJOeJcnbJeLeVrm0yc7BcdTAwMTdP9e4y3jzI9C9u8zVcdTAwMDMhN4RBL/uiUWvrRurd+SwuuiOSdJxcIsKb3I3rXHUwMDAwaHjBZ+G6KHlcdTAwMTWfeFPsRH20XHUwMDEx9FtcdTAwMThN27ua9duVxure6nlnuXZb3ln7xZ27Z0trXHUwMDEwj9GOlCNnf4drNblHXHLvXHUwMDE1XCKhJrj1QoJrNWl00jrxO0b56aduxm9sXHUwMDAzT1x1MDAxOFx1MDAxNVx1MDAxNFneUU58mOnKXZu9W1x1MDAxM+srOzv32Vx1MDAxNYzd2fblYV58XGKh2lx1MDAwNki+UlxuirROR49cdTAwMTjtl8FpRVxi9U5JQb/d51LDxXJJlfLjXHUwMDAwlcR1pVx1MDAxNUSB0ViylkhkXHUwMDFiJDYwMF4765ltSHKYalx1MDAxNKLIXUeV/Uo3+nJholxuM2m9d5KRT1ukXdcvU3p143LvcDZcdTAwMTX257T33czVTHhSf+q08+Ji9XBv/ab31PgqdUdwILo+XHUwMDFm51x1MDAxZp/aIOZsXHUwMDE0yvdcdTAwMWPEuFJO5eHo2nWaZ49yZb1X2c2sJZ6gXHUwMDEwOVx0LFx1MDAxYc3HL1x07q0w3v1JXGLD59xcdTAwMTlF/jW5PfRcXL9kxSyc85+6Ic9xu1x1MDAwZfOOKuiLsL0s072101x1MDAwYpHN71x1MDAxYtvJp5dlsp0/qdxcdTAwMDClXHUwMDA04tDcjlSPIJS9v6BcdTAwMTBoiK55Jz/ZcFxcXHUwMDE1TbkyIVx0XHUwMDAx3lx1MDAwN9JbXHUwMDAwj9yHSkyo3yDvT+BFQzpUOOWjNVx1MDAwNT8hXG5cXH6iXHUwMDE27oCmaD/xXHUwMDExiEq2SVx1MDAxZF1cZn1cdTAwMTOhd729w4vl/HZnZ2VHnl2J1XVI/uI16lx1MDAwMKxcInhwP3mMXHUwMDFlY/HcidRcdTAwMDSSXHUwMDBiMFx1MDAxNOk8XHUwMDFmLeBJmlx1MDAxN1x1MDAwNU6kWLt4zSHIaOMwalx1MDAxNUU4mozZI31ntbZXrbWrdrmi0o3Udc2ellx1MDAwYknHKFx1MDAxYVx1MDAxOWiB5JpcZoAzY/2cTUBcdTAwMTSagihywjfBzXKl5lx1MDAxM7VcZs4tXHUwMDEzXHUwMDExab/4rdkygFg2yrkh3nw/O0Srayd+67GweplXXHUwMDA3T83zfPmweFx1MDAxOXcqeEJcdTAwMDK9dC6g6GmRmDc5Slx1MDAxY85DXHUwMDE4TVx1MDAxMVihUJI7zmvxPTJcdTAwMGZkoIhE8mJcdTAwMDFZgvGRj1x1MDAxOVx1MDAwNHqv+Cwzy1x1MDAwZV8hUZLxxTbB8mHo1d8p875Vjs3TXHUwMDAyplfBWURcdTAwMDPvqDG6gHSx2Mvu11x1MDAwZdPiuuuf0ie4XHUwMDFld1x1MDAxY2lC1jPYXHUwMDA0LHeC9rxcdTAwMWRFXHUwMDFhXHUwMDFjWXAjscY9KZSxRDC9/tySRizXhcCQtCBkU5RcdTAwMTAmmltcdTAwMWGsaSA3vFx1MDAxNMJb0d8zM8Z1uaU198iYz3Lz91pcdTAwMDBxh1xiw/2kXHUwMDA1dMq9zkTw2/hcXFx1MDAwNDvDfmO5mbF/WMrc7V9cdTAwMWTpjebVQ+8207kut+1jsrfAXHUwMDEw5lx1MDAwM97agqiJXHUwMDE1Kj16yKly/eI7XHUwMDE0ktgpfvKs6Im1S6gn8JHoyVx1MDAxNi+bWYWWnjuKzVx1MDAwNdrftbnlNH1oiqliPsy1ZK+3msvmunj8y4LGXHUwMDAwmY1657j21K8uckOPpvO3tfBxXGJdfVuiXHUwMDAx3tdanW4+vGzTXHUwMDBig5c7XHUwMDE3uf3tMlxyoP+Oeuily2Htik3vR1iuXGbbZKdGXHUwMDEz83q504is4Vx1MDAxNGkoeXq71lZp9Cs1WrWrWj1cdTAwMWaeTFx1MDAxOdZU3/A80Vx1MDAxM5yDjO9cdTAwMWLG68rkqmdcdTAwMTfY01x1MDAwMfVcdTAwMGLKXHUwMDFh3/RcZlrYgMNcdJLxK1x1MDAxN21o1H+51IHU5Fx1MDAxYVR/Z4P83Fx1MDAwMtXkosZAcDrfSevpfy0mXHUwMDE0NVwi14EpxedcdTAwMWIrjUpGT1x1MDAxNXshhY6rXHUwMDAwXFy0XHUwMDE3cZKES2TW8q3OSq1eqtWv6OLAdfwoP3/01lxmXHUwMDExpm+zxS6PMiVcdTAwMDLPRf2aj6ZWpP5cdTAwMDbHXFzwXHUwMDE05JvPtFuS6lx1MDAwM+2lN8R7lHt5xqtcdTAwMGb7Ua6X3lx1MDAxZdT0nZ/RQUFAZkM3RIO1Rlx1MDAxYTM+Jkk0qK+6pNbWXHUwMDFib8aGXHUwMDE05tud1cbtba1DU3/QqNU7o1Pcn8tltupqOT/mL+grRa+Nmn+T33E4Plxm/rY0sJH+P17//p8/Jz47XHUwMDE1i+H+1XH4XHUwMDBl3vCP6J/vdl3g4lx1MDAwZsxcdTAwMTQ0rzp6XHUwMDE4+Fu+a3rQSqTvXHUwMDAyXGacMoq4hTPK4OjiOlx1MDAwMZ9cdTAwMTNcIkKTojTuk1x1MDAxNXRcdTAwMTNJTUBKQTjuXHUwMDEyQoJcdTAwMTYnkXn0JvBoXHUwMDE0N1j2Q2vJr9VcdTAwMWbgSVx1MDAxMkj8f+a6ROCclyTDvFx1MDAwM7qFkSbdr16CXHUwMDBi6lxy3TuCOadcdTAwMDOMn+64hr/F7+Q/YnHEP2NcYnqn94hcdTAwMTNFXHUwMDE4aVx1MDAxYzbqPKRA8lTvKM5pZFx1MDAxZs9ubaPcLGbO2/uq53Xq9EOFXHUwMDBm89NEzlx1MDAwNzTd5LWVdlx1MDAwZYRcdTAwMTjO2fJOzUDxjk3pnJRcbj/XXmWy+1AzaVwiXHUwMDEwXHUwMDEyrHZfeLTJK4K+dmP+VE10eUfM/na1dLV2SVx1MDAxYXPz5GB/q1hdXHUwMDA0TfR8N5MmiZ5H9TFagT5eXHUwMDExkSP22pnZN8xOx1NcdTAwMTJphVTE5/hcdTAwMDRuzV1cdTAwMDEs4qhnQKJcdTAwMWRcdTAwMTSUXHUwMDE09y6Unz1cdTAwMTJpomdcdTAwMDBhXHUwMDAyR1x1MDAxZu1IgFx1MDAxMmsk/TXuKIhCoyT2Yzw3K3U+kjP5mUyxzlxuObfVxm/lXHUwMDE101x1MDAwM8wrZfBcdTAwMDF6a9FYllx1MDAxZdxcdTAwMDd7smhcdTAwMTKcXHUwMDA146JcdTAwMWPBZbdK/oypXy2Jnlx1MDAwN4UuXHUwMDAwzjtznLGSXHUwMDBmMpqsmoRcdTAwMTe8QEKciIZlzLhO+51ITTyE+Sc1jt530pp474XxR7p5x4fcvKPubHrUSqL3sjJwJHZI+TG43UhCXHUwMDA3+KhccuuE1EZ5bsvyXHLbVEFDXHUwMDAwXHUwMDE0XCJcYsaOTFxm3KR1voC7oDrSqJJbbMrIdtSfzotgQaFmfodf/1r3XHUwMDE1cU5AhFx1MDAxNEi1XCJcdTAwMGIk7uw35lC48Vx1MDAxZlqFkmjhc9Ob6f7r91VG8Vh6vjxcdTAwMDajd3qROHFkpm3MRXDeR1Olb/ZHrW489MJcXObx8WlVy+WM0vXK2q/0XCL+LS+S0nyomXL9XHUwMDAzd4zGkcMktFS8nmRRXHUwMDBiukNexi+WXHUwMDE2PeYx/43iqH/ajv9KdTStfppl+mBcZt+tM0hJVIJ2sdVcYsPLRqXSLidi6WXCqD5cdTAwMTapdXzrNG1cdTAwMDRZdZS8vmVj03dFJzFSp6znXHUwMDEzqsnXW66vJa8/ZGOqf4C1JetcdTAwMTJGO4VTemd+2MhcdTAwMDBVYJXnXHUwMDE0XHUwMDFjSC8hsmFrXHUwMDEwqo1cZri3XHUwMDA3elSWrHIsg8l1Q4pcdTAwMGL95tLyXHUwMDA0rDPqK0L1aFB7O4hPb1oywuWl53RcdTAwMWVqcpXCXHJK/pZcdTAwMDbJTU/S0mrF7W608vL3ZvuxQOpfXHUwMDFkhdBcdTAwMTdF6eh+37H6fetcdTAwMWRzg9mj9NHZzX796v60dt29yOS1PCnBdpjsKI3OkGrkQ1x1MDAxNLRA6e1o3SmRJ6GF8WSeXHUwMDE433huXHUwMDA29zG1rSmYXHRcdTAwMWK2JoRqkoOMdfi6XHUwMDFh/VdcdTAwMWP9ilx1MDAxNjFzTDg+Lv176SXiPiaBXHUwMDAyXGaN52PB30yR6aRrjFx1MDAxNPJcdTAwMWR1XHUwMDE3U+91XCJNXHUwMDE3hFxulCNpY6Xpn1x1MDAxYTmyOYyrZT2FXGLe4m7UlMqLT1uv8oHzvHvGcz9cdTAwMTIhJ3BcdTAwMDBSYVx1MDAwZcBcdHR82ICa0Fx1MDAwMseK/smyZi6rmGA1XHUwMDA1ky/gXHUwMDAwcZF+eihYiq5iUpwnXHTCfcOF9laPXHUwMDA3elBcdTAwMDFdI3fBrWelNm682OFr2Vx1MDAwN9FGQYzCsjDiqr5J3IN71HE9oHRa0vB/b+5cdTAwMTFcdTAwMGJg/kmNYfeLyIeRscVcdTAwMTc0XHUwMDBloKDrZ+9cdTAwMGaDXHUwMDA3qdZaeLjse2F2I1u73c6Bc8l2YOSZXHUwMDAydl7kw4zhxZAh/+UlXHUwMDA2SE5cXDllLZeifJ//slx1MDAxM1x1MDAxY9Yk9tE/1O1cdTAwMGKXQ6aRj0/sXHUwMDBlf5t8XHUwMDEwnfaRpv7fTT56g2DfS1x1MDAwMvlcdTAwMThcdTAwMWHPx8hcdTAwMDeXp8VcdTAwMTkvzbAw7zr+aPrNTqTxXHUwMDAy9yVBpygmeTJgXHUwMDFjyT2QTlx1MDAwYlx1MDAxMNGT/Vx1MDAxYe99fO1DibSHqHzCeJ1cdTAwMGZQsCZUxnKX7HFTNuTdjdJeKvLfWsFY41x1MDAwN7DAgfUri8a/xfhm5Fx1MDAxZdNDQSSGXHUwMDBiJTiuXHRyr9xBYFx1MDAwMlx1MDAxM+CVTor0XHUwMDFj4ZWy3HOByPHH2Mf0ziZcdTAwMDOyM2FcdTAwMTTc5Vx1MDAwMFx1MDAwNaBzgLxs/nuTjTjA8k9qXGarX8Q1tI7lXHUwMDFhUvSLe9/RwnzdbKz2uqc5dyGOb25qVX1cdTAwMWFmk801NK9cdTAwMDKR7XnBp11ZO9xcdTAwMWKaZjvgynVNXCKK7suUXHUwMDBl+5+lXHUwMDFhOKk63Y4lRHmbL1x1MDAxOdxcdTAwMTdcdTAwMWVcdTAwMGU8jWp8b8NcdTAwMThUVnxIaX2aaiz91//Wn5dcdTAwMWGmV1mZ4beZJ/uYNMRcdTAwMGZcdTAwMTJcdTAwMTLw8VvUKOKAtO85pWA6Jlx1MDAxMmni3lx1MDAwN55z4ZJcdTAwMWKB2JGaK1wiXCKBRd7VzkdcdTAwMTR4XHUwMDFiT0g+rSbIv5NIXHUwMDA3329RJGDSKqTl4lx1MDAxNW208dKSjejx7Shk/PRiO5+OfsinWOiP2OiMjGR6wFiKZlx1MDAxZZDPylx1MDAwND6uwFC8x/GdXHUwMDFmKuBSW3IqgMhV31x1MDAxZt2MMnVcdTAwMGL+0JCI8lx1MDAxMNEl5Vx1MDAwZp6X7u3YkFxcoPlwXGbNjVx1MDAwZpif/N51V6l4XHUwMDA09y+PgfeLKFxuQPwxcyRcInjh/Fx1MDAxZOdg1dxGXHUwMDBmM+kw18hgunS0JY5cbvvbiXZg4HSgNVx1MDAxZj/FvJDsYPR4XHUwMDAwXHUwMDEzOEt6SkpyXHUwMDBicko70lx1MDAxOc7BmurB7IQ2uZFcXPvPTlx1MDAxZlxc1+Hn2C/p06slkfa/QyxiXHUwMDEwKX6yiJVGvlVabjYn0oXI28yDLryO5dmo/nix21x1MDAxZvlm87hDs/Tq42j+a6WXrzp4x1x1MDAxZve18sPK5OV8XtH/48VQ2VwiynxcdTAwMWL++vuPv/9cdTAwMGZcYuGAVyJ9 virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp"},{"location":"guide/widgets/#region-updates","title":"Region updates","text":"

    The Line API makes it possible to refresh parts of a widget, as small as a single character. Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive.

    To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer. Here's the code:

    checker04.pyOutput checker04.py
    from __future__ import annotations\n\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Offset, Region, Size\nfrom textual.reactive import var\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\n\nfrom rich.segment import Segment\nfrom rich.style import Style\n\n\nclass CheckerBoard(ScrollView):\n    COMPONENT_CLASSES = {\n        \"checkerboard--white-square\",\n        \"checkerboard--black-square\",\n        \"checkerboard--cursor-square\",\n    }\n\n    DEFAULT_CSS = \"\"\"\n    CheckerBoard > .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard > .checkerboard--black-square {\n        background: #004578;\n    }\n    CheckerBoard > .checkerboard--cursor-square {\n        background: darkred;\n    }\n    \"\"\"\n\n    cursor_square = var(Offset(0, 0))\n\n    def __init__(self, board_size: int) -> None:\n        super().__init__()\n        self.board_size = board_size\n        # Each square is 4 rows and 8 columns\n        self.virtual_size = Size(board_size * 8, board_size * 4)\n\n    def on_mouse_move(self, event: events.MouseMove) -> None:\n        \"\"\"Called when the user moves the mouse over the widget.\"\"\"\n        mouse_position = event.offset + self.scroll_offset\n        self.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4)\n\n    def watch_cursor_square(\n        self, previous_square: Offset, cursor_square: Offset\n    ) -> None:\n        \"\"\"Called when the cursor square changes.\"\"\"\n\n        def get_square_region(square_offset: Offset) -> Region:\n            \"\"\"Get region relative to widget from square coordinate.\"\"\"\n            x, y = square_offset\n            region = Region(x * 8, y * 4, 8, 4)\n            # Move the region in to the widgets frame of reference\n            region = region.translate(-self.scroll_offset)\n            return region\n\n        # Refresh the previous cursor square\n        self.refresh(get_square_region(previous_square))\n\n        # Refresh the new cursor square\n        self.refresh(get_square_region(cursor_square))\n\n    def render_line(self, y: int) -> Strip:\n        \"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\n\n        scroll_x, scroll_y = self.scroll_offset  # The current scroll position\n        y += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\n        row_index = y // 4  # four lines per row\n\n        white = self.get_component_rich_style(\"checkerboard--white-square\")\n        black = self.get_component_rich_style(\"checkerboard--black-square\")\n        cursor = self.get_component_rich_style(\"checkerboard--cursor-square\")\n\n        if row_index >= self.board_size:\n            return Strip.blank(self.size.width)\n\n        is_odd = row_index % 2\n\n        def get_square_style(column: int, row: int) -> Style:\n            \"\"\"Get the cursor style at the given position on the checkerboard.\"\"\"\n            if self.cursor_square == Offset(column, row):\n                square_style = cursor\n            else:\n                square_style = black if (column + is_odd) % 2 else white\n            return square_style\n\n        segments = [\n            Segment(\" \" * 8, get_square_style(column, row_index))\n            for column in range(self.board_size)\n        ]\n        strip = Strip(segments, self.board_size * 8)\n        # Crop the strip so that is covers the visible area\n        strip = strip.crop(scroll_x, scroll_x + self.size.width)\n        return strip\n\n\nclass BoardApp(App):\n    def compose(self) -> ComposeResult:\n        yield CheckerBoard(100)\n\n\nif __name__ == \"__main__\":\n    app = BoardApp()\n    app.run()\n

    BoardApp \u2585\u2585 \u258b

    We've added a style to the checkerboard which is the color of the highlighted square, with a default of \"darkred\". We will need this when we come to render the highlighted square.

    We've also added a reactive variable called cursor_square which will hold the coordinate of the square underneath the mouse. Note that we have used var which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor.

    The on_mouse_move handler takes the mouse coordinates from the MouseMove object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down.

    • The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. We can perform this conversion by adding self.scroll_offset to event.offset.
    • Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.

    If the cursor square coordinate calculated in on_mouse_move changes, Textual will call watch_cursor_square with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. The get_square_region function calculates a Region object for each square and uses them as a positional argument in a call to refresh. Passing Region objects to refresh tells Textual to update only the cells underneath those regions, and not the entire widget.

    Note

    Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.

    The final step is to update the render_line method to use the cursor style when rendering the square underneath the mouse.

    You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red.

    "},{"location":"guide/widgets/#line-api-examples","title":"Line API examples","text":"

    The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration!

    • DataTable
    • RichLog
    • Tree
    "},{"location":"guide/widgets/#compound-widgets","title":"Compound widgets","text":"

    Widgets may be combined to create new widgets with additional features. Such widgets are known as compound widgets. The stopwatch in the tutorial is an example of a compound widget.

    A compound widget can be used like any other widget. The only thing that differs is that when you build a compound widget, you write a compose() method which yields child widgets, rather than implement a render or render_line method.

    The following is an example of a compound widget.

    compound01.pyOutput compound01.py
    from textual.app import App, ComposeResult\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\n\n\nclass InputWithLabel(Widget):\n    \"\"\"An input with a label.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    InputWithLabel {\n        layout: horizontal;\n        height: auto;\n    }\n    InputWithLabel Label {\n        padding: 1;\n        width: 12;\n        text-align: right;\n    }\n    InputWithLabel Input {\n        width: 1fr;\n    }\n    \"\"\"\n\n    def __init__(self, input_label: str) -> None:\n        self.input_label = input_label\n        super().__init__()\n\n    def compose(self) -> ComposeResult:  # (1)!\n        yield Label(self.input_label)\n        yield Input()\n\n\nclass CompoundApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    InputWithLabel {\n        width: 80%;\n        margin: 1;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield InputWithLabel(\"First Name\")\n        yield InputWithLabel(\"Last Name\")\n        yield InputWithLabel(\"Email\")\n\n\nif __name__ == \"__main__\":\n    app = CompoundApp()\n    app.run()\n
    1. The compose method makes this widget a compound widget.

    CompoundApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e First\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0Last\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0\u00a0\u00a0\u00a0\u00a0Email\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    The InputWithLabel class bundles an Input with a Label to create a new widget that displays a right-aligned label next to an input control. You can re-use this InputWithLabel class anywhere in a Textual app, including in other widgets.

    "},{"location":"guide/widgets/#coordinating-widgets","title":"Coordinating widgets","text":"

    Widgets rarely exist in isolation, and often need to communicate or exchange data with other parts of your app. This is not difficult to do, but there is a risk that widgets can become dependant on each other, making it impossible to reuse a widget without copying a lot of dependant code.

    In this section we will show how to design and build a fully-working app, while keeping widgets reusable.

    "},{"location":"guide/widgets/#designing-the-app","title":"Designing the app","text":"

    We are going to build a byte editor which allows you to enter a number in both decimal and binary. You could use this as a teaching aid for binary numbers.

    Here's a sketch of what the app should ultimately look like:

    Tip

    There are plenty of resources on the web, such as this excellent video from Khan Academy if you want to brush up on binary numbers.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaU/jyFx1MDAxNv3ev1x1MDAwMjFf3pMmnlpvVY309MTSQCCsYe2nXHUwMDExMomTmCTOYidcdTAwMDRG/d/frUBcdTAwMTNncchcdTAwMDKM0400PVx1MDAxMNtJueqcU+de36r8/WVtbT16bHrrf66te72CW/OLbfdh/Xf7etdrh34jwEOs/3fY6LRcdTAwMGL9MytR1Fxm//zjj7rbrnpRs+ZcdTAwMTY8p+uHXHUwMDFkt1x1MDAxNkadot9wXG6N+lx1MDAxZn7k1cP/2n+P3Lr3n2ajXozazuBDMl7Rj1x1MDAxYe3nz/JqXt1cdTAwMGKiXHUwMDEw3/1/+Pfa2t/9f2Ota3uFyFxyyjWvf0H/UKyBko++etRcYvqNpVx1MDAwNDQ1lHP6eoZcdTAwMWZu4+dFXlx1MDAxMVx1MDAwZpewzd7giH1pXHUwMDFkzppcdTAwMTdnwVOnSW865dxWZn9jc/dm8LElv1bLR4+1frNcbu1GXHUwMDE4ZipuVKhcZs5cYqN2o+pd+cWoYlsw8vrrtWFcdTAwMDN7YnBVu9EpV1x1MDAwMi9cZoeuaTTdglx1MDAxZj3amySvLz53xJ9rg1d6+Jek2lFEU05cdTAwMTRcdTAwMDHDxetBe7XQ4GggWjNcdTAwMTCEMdAjrdpq1HAwsFW/kf7PoF13bqFaxsZcdTAwMDXFwTmiXHUwMDAwXklcdTAwMGXOeXi5VymoQzhjnFHNKSHy9YyK55crkb01RVx1MDAxZInNUJJcdTAwMTEmqVx1MDAxMmzQXHUwMDEyrz8mXHUwMDE0NKGcXHUwMDExMlx1MDAxOFT7+c1ssY+Pv+I9XHUwMDE2XHUwMDE0X3os6NRqgybbXHUwMDAzX2OYXHUwMDFhXFzTaVx1MDAxNt3o5WOUUoZQrYlcdTAwMTn0Rs1cdTAwMGaqo29Xa1x1MDAxNKpcdTAwMDO09F/9/vtcdTAwMDIwpYQn41RcdTAwMTNcclx1MDAwMsDMjtM9dpJxm72oVT047Fx1MDAxYz1cdTAwMTGze8C/Lo5T9k44xWF/XHUwMDEzqMrBW+VcZlxiUcA4XGYjVVx1MDAxYYdcdTAwMDHiR1HBXHJ2i1pcdTAwMDaqUdtccsKm20YkTIKrcLBcdTAwMTlcXHBcIimlXHUwMDEz0MqBOJpcdTAwMTIjXHUwMDA0cI5cdTAwMDRjMIpWMJJIzpn8NLCaT1x1MDAwMWucmKNY5VpQI1x1MDAwNZiZsZrb2bvZolx1MDAwZlutWq8rRHGDkqfrIFx1MDAwMasjePsnUUqIXHUwMDAxhVomULFGQKpcdTAwMWSthcbxXHUwMDAw0Fxu4CNBylx1MDAxZEJBMi1cdTAwMTVVRulxlDLtXHUwMDAwXHUwMDEwjlx1MDAxYU9cdTAwMThwS6xRlCrNSJ9Sq4dSr1bzm+FEjIJcdTAwMTKJXHUwMDE41YIowbWSM2PU69ZPN77dXHUwMDFmXFzlstmb6kaudbW3XV5cdTAwMDSj7zXjz4BRXHUwMDFjeUMkXHUwMDEwoVx1MDAxMa7EsGGQXHUwMDAygpRLg8rGjIXOUnN+yZVMsnF8Uu5YRyFccuCEzpBcdTAwMTJiXHUwMDFjoJQ5XHUwMDEyQFx1MDAxOJRQwlAshVx1MDAxYVx1MDAwNShcdTAwMTeUMWpibfxcdTAwMTlcdTAwMDCqmE5cdTAwMDIo+jBcdTAwMWMxodTM+DSsV7nNRlx1MDAwN7nuUfd8v51vcdVopFx1MDAxY59SXCJcdTAwMDJcdTAwMDFcdTAwMDEqXHRcdTAwMDe0pSP4VFx1MDAwZVCBRGVUSFx1MDAwNVIsXHTQO3ScXHUwMDFmXHUwMDA1UIZyYlx1MDAxOFx1MDAwMbWCXHUwMDEz/TSE6uRpXlKMXCJcdTAwMDSfXHUwMDE5oHv5ulxmy2euOLmqXHUwMDFmh8F5buN6p5NygGqOYVx1MDAxMZM4W0itXHUwMDE4lyNcdTAwMDBcdTAwMDVcdTAwMDdRY7hmiFWtR83HfPik7E5r+Dh8XHUwMDEynFx1MDAwNriWn1x1MDAxNzV9jlx1MDAxMdUyXHUwMDExoFx1MDAxOERcbowk+excdTAwMTBccjJnV0FwuXta7mZ36MZGePh0t5duiCqG8ZA0SmBgLVx1MDAxMYxmXGKiUjJcdTAwMDSwZIDgXHUwMDExeFAuXHUwMDA10VKpNFx0n1x1MDAxM1x1MDAwMFx1MDAxOWPKj1x1MDAxOVxccpzCcVx1MDAxMp9cdTAwMDN/P5AwwFx1MDAwMn955XsyLF+vXHUwMDE5XFxcdTAwMWTDUuT1XHUwMDA2Jjo28s2Th92jTKV0obY297fJ6WbLO9Drr+d9/33y2z5f/PW47rmt/Xz1kGarXHUwMDE33lGzenFcdTAwMTJcdTAwMGV/yo/Pd9vtxkPsfV9+S+KSxrBSsHgotSSXhu4/RiNhXHUwMDEyaaRBgVx1MDAxYVxuYd+i0eTOnEyjiluodNpe+omk3o9IU1x1MDAwMzrGx+nExuiEXHUwMDAxNjZCXHUwMDEzmc6QbTDWjSDK+099S0uGXt1x637tcWi4+uDE/lkz8e5cdTAwMGI9/LxnJFx1MDAwZZ25UfPLXHUwMDE2uus1rzSM6cgvuLXXw3W/WIzPXHUwMDFkXHUwMDA1/HBcdTAwMTffsZ2dRfNcdTAwMWJtv+xcdTAwMDdu7XzQtsUnK5qciTZCXHUwMDEzYSPSmVnmV0/Oj2vlzMP+9l6tK+tHJrpkqWeZXHUwMDExyDLGNNpcdTAwMTFcZnHYsOVHmXE4xnnKXHUwMDE4jFx1MDAwYthcdTAwMDfm9lx1MDAwNHdsXHUwMDFhXHUwMDFjuKVcdTAwMTBIOSm7x5hcdTAwMDPKSGyyTeRQYsbzJngpXHUwMDFlg3nC0oUmtXR4LWqSXHUwMDEzKqibIONcdTAwMDH6W/DduoON6FwiMsXb++vTXFzwcHK2XHUwMDE1h9o/9Vx1MDAxY+VtXGJrKlx1MDAxZI6ux3CDs7PUZlx1MDAwNMLCMTgmXHUwMDE4XHUwMDFhUUYwMljKcVx0UiBSTZokXHUwMDFj++Rcbj9cdTAwMDQ7XWpKJ6CXOlJcdTAwMTJF7dRtjb9cdTAwMWVcdTAwMDUvN4JK8vGGLFx1MDAxZNhlsXFcdTAwMWRPWHOFgVx1MDAxM5394cp+b/eCXHUwMDA2N1t1cXpPj8J6JnNZ99OuvUZcdTAwMDFcblx1MDAxYdpw9DFCXHUwMDFiMVx1MDAwZVxcITHg11x1MDAwMiNdJZeKZj9Fe5F7XHUwMDEy47t/XHUwMDE2v/Fu/1D8isRAVzFcdTAwMTQhXHUwMDE5XHUwMDFmz7fgXHUwMDFifVx1MDAxNU2/c1jyXHUwMDAytl1RXHUwMDE5kTO79HZcdTAwMTW0XHUwMDE3J1x1MDAxOFx1MDAwN4HKudFcdTAwMTKNOGUjXHUwMDEwXHUwMDA2h0hcdTAwMDGaXHUwMDE5gtJrlkHwh0qvfZzLmFwiep5s4TtGw88oyOe/XHUwMDFkdO/Pe3dunlSOXHUwMDAyUz2JVP3dwlb0d7FHt1x1MDAxZkpccpmYRqdAUTw4mUPaN/d6XHUwMDA10T5Sgp1vfDvdr9Jet1dLu7RjbzuUY8dcdTAwMTNcdTAwMDFExaPCV1ttn1NcdTAwMTP0JahasFxmMT5B2pmy8bdcdTAwMTK/iLRzQVx1MDAxM/HLJEhOyVx1MDAxY88p71xcssd446RVydz1mt6J37qst1ZB2y2G0XegfFNcdTAwMTR3zSeEhuhaKMNAg/LlQsPfNGjPTEi2v4e4o61cdTAwMDai8YRfXHUwMDA0vclVS1x1MDAxOP9cdTAwMTjCXHUwMDE5mT1cdTAwMDHf2rlcdTAwMTf+dnBcdTAwMWJcXJU293dP+d7N8U1SIUhqxFx1MDAxNyiaXHUwMDEyXGb4lFbow1x1MDAwMMRcYnCFI21lndBcblx1MDAwNZGnPadhn2VRXHUwMDFlL877qeGLTE9cdTAwMTRfyYnSqFx1MDAwNLPj9+yqVbx42pG128JZVVx1MDAxNKNm7f6gsFxu4lx1MDAwYlQ7XHUwMDE0XHJcdTAwMWJojFx1MDAwZlxy3vN4bKhcdTAwMTDZwI1SiObUii9lXHUwMDFh8Dao+UXgy03yXHUwMDAzepuZxLBRzVx1MDAxZVx1MDAxNz5cXFb35bedyq6f9erB8VngPVx1MDAxY6Q+rYF65lx1MDAxMCptvVxmXGLsj9G0XHUwMDA2c9D14pQsXHUwMDE1pfFAIJ3ySzlGK4LJX1x1MDAwNcBCJeY1NKP2QVx1MDAwMZ1dfm9Ubkfz4lG1ZIr5Mjts1VtVs1x1MDAxMvKr7MNHNPpoXCK0UWN5XHLEsCRGM2LQbi1ZaPKh8quFXCJGxiOan1x1MDAxYr06Oatsi4Ml9sbsqYf7w1x1MDAxM9K4PLy99MONh43QXFxH+fOktFxcauRXceUwjNgwNiNcdTAwMDTFS41AlztSoCnGIaGGkbRnle1iIFxmt3+Z3IOEZP+A3Vx1MDAwNMJQmD33kMtcdTAwMTauvcr1TeusXrjNPmRcdTAwMGav+fHxKuivXHUwMDA1sTRacS2YfVx1MDAxNqTHQUxt9lx1MDAwMf1cdTAwMTSVii9cdTAwMDPij9VfyVx1MDAxOcM2/ir6K6eU+INcdTAwMTbGMJgjdVbee3wqXHUwMDExt37VaoS3hzdcdTAwMWI8W8Omp1xcf7VE4bOVSURb68BGrUNfXHUwMDE2gSphUcGXeibyXHT6y4kkiH4+T5nqXG7jXHUwMDE3ZGL2gffLis1cdTAwMWNld1ftzVMhzEGvfXfwKFx1MDAxYrVcdTAwMWJcdTAwMWFcdTAwMTb2V0F90Tw4VKBNsE/0uIxVwP2AMChbXHUwMDFiheBcdTAwMDK0walVX4bmXFxY+/CLgJcmi68xVGCwMrv3velcdTAwMTVOdffkXHUwMDAyrlx1MDAwZnfunlqd/cvk1Vx1MDAwManRXjSLqL3CYM8zNbR+91V7cUKWiFrN4s44ndqL5uHZ//xcIvCNXHUwMDE1q43AXHUwMDE3XHUwMDE1XHUwMDAwea7nSPxcdTAwMTa7VffxdrtcdTAwMDS3j3fu08XxiV9/zK6C9lwiT1x1MDAxZEJcdTAwMThcdTAwMDDKL1omOlx1MDAxZb4pZp8/UvtoWaU48Vx1MDAwYii9gPj+eZxvUq0+Jcm6S23yXGKHi8yM3MKW2cs8VkpcdTAwMTc7ep9lg9Pc0Vx1MDAwZZUrgVxcw1x1MDAxY3RcdTAwMGKEXG4lXHUwMDEwejCCXFxcdTAwMDKOXHUwMDEyNppcdTAwMDctUZ2TXHUwMDBi9lx1MDAxMTeuWFx1MDAxMLmxRN1cdTAwMDCqZFx1MDAxNJv9dYHA4lx1MDAwNYbLXHUwMDE2679cdTAwMDJ3Ql1P7mJcdTAwMGZuu7RLOlx1MDAwN/eiXFwq34WN2FQ6XHUwMDA0sbnrelx1MDAwNKNcdTAwMWFiOYPFllx1MDAwMsQqUN5YXG4wNC6DlVx1MDAwMGLoxFlXXHUwMDAyRI1m0jKAoVx1MDAxYlx1MDAxOK35J9NL/pN4XG7JZUmKISbto6qZaXr+UKrlNk0+XGLK7u5Fdvss4zG6XHUwMDEyNLXPXHUwMDBlKWFcdTAwMTjWac3VyMNxXCLt7MPxP1xmX5VKtkjLsHSSXHUwMDFiXHUwMDFhI6mdgDBcdTAwMTD5+PU0n09cIppcdTAwMDZcdTAwMTLRxUhERTKLXHUwMDA0XHUwMDAyRtrtNWZmUY5+bfaK1b2C2JK1m8JJt83yuVVgkVbELoanXHUwMDA2JzOiOZ1AI0BJsfGzkmy0XZ862VEmXHJX8bTxz8Mkllx1MDAwNiaxXHUwMDA1mVx1MDAwNIllhuhcdTAwMTjR/+NcdHpmJu2dbXSzl5fhrp/fyarr6Lq3ebGzXG5MwlDGYVLYXHUwMDFiRnuoKYwxSYKtz5bckPhjoHdl0qRcYmeMSZRcdTAwMTOpOPC5kqGrwiSRXHUwMDA2JolcdTAwMDWZlFxcs6BcZlx1MDAwN61cdTAwMDWfPf66XHUwMDBmz13aOFx1MDAwN5/Ip8fbsOh+O6+vRNZcdTAwMTZcZjhcdTAwMDLjL4M84dyIcVwiXHUwMDAxKENcdTAwMTkwW8r9z1x1MDAxMkmjs/uUzS8+n0gyXHJEkouau+RtXHUwMDA3XGLaXGKGnTk7k85uy3tQb3qV7Y2n3YjSXG5/MKvBJC5cdTAwMWRFtX28rFx1MDAxNWej1T9cdTAwMDRcdTAwMWPWL4RcIooyPeVcdTAwMTHexzOJc6FcdTAwMTVTn7BR3OdcdTAwMTNcdNJAJFiQSDzR22HQIKyhmWNKOi+b/MmFyLiFfWRJJLdcdTAwMWW71XBcdTAwMTWIJFx1MDAxNbo3YiTBXHUwMDE5WFx1MDAwYi1GyjhwSuKcUlx1MDAxMFx1MDAxY12VgOQyjqWIRGYhkt1yz65cbvhcdTAwMTlnJJVcdTAwMDZcIqlcdTAwMDWJZJL34Fx1MDAxNVx1MDAxYeNaXHUwMDEy31XyLVwi8XLQpHukWij1NiumSup1fytpOUuqiKQpdZRdUkw44lxcqdFcdTAwMTlJONTuzStcZmg2bUnLx6dcdTAwMWJcZlx1MDAxMcauqvlcdTAwMTmJxNNAJL4gkZLTdlx1MDAxMuyihDm2XG7ptIS/V2td3T1G3V3voFx1MDAwYse3Okqg0WLbslx1MDAxNd2w4r3zdlLaIVx1MDAwNqdcdTAwMTljjJB0JDrCOdlcdTAwMTFcdTAwMDKhi7GRYYpM2XyVgyhcdTAwMTXUdFxuTdyYTc6U9pbcSPGu+1a+XHUwMDFjWIGd0pZjZzZodqJ//Ttcclx1MDAxY/3RlKlMfe7QXHRUTa5BXHUwMDEz9lx1MDAwYlx1MDAwM0Do2esgplx1MDAwZnA6d1CU4IDk6Fx0cUJDYzi8fkhcYuZQTe3G2VxiMymSXHUwMDBi2Fx1MDAxNyYqY1x1MDAwZfpVQFx1MDAxOVDcME5iNVx1MDAxOINcdTAwMWQ+ibGb4jNb1Gp3XHUwMDAxZWPfi1x1MDAwMFx1MDAxYVBr9Fxc+/ksOlx1MDAxMVwi1WQ8mzJcdTAwMDfVwshtR5t+UPSD8vpQXHUwMDA1xsu3fGRnkPw+OVx1MDAwYlx1MDAxZNtK4lx1MDAxMG1QSXH0XHUwMDAwhUzSwY5Ltlx1MDAwYtymXHUwMDFkYUdcdKLQunGFWGaoxi9nXGaqPLyg+Hajpm9cdTAwMTVcdTAwMTdrVIZYzFx1MDAxOIyUXHUwMDE5XG68xMbJsUZRh1GluMBwXHUwMDA1zyGcwVijam5cdTAwMThtNdA0Rtj5J1xyP4hGO7nfm1x1MDAxYpbYXHUwMDE1z1x1MDAxZJNcdLyp+LFRXHUwMDA1aNp3XHUwMDFjluzBb2tcdTAwMDOW9P94/f2v3yeenVxmYvszXHUwMDBl38H7fYn/f26bkVx1MDAxOPditCdcdTAwMTiXc+z+ynVR+81vN6Ql/bPcXHUwMDE27+RPL5LyRynRLiPAkYaAfC4vieVl7PVcdTAwMWGYY5fzckSixDebkjlaVLtgtlx1MDAwNKyiknE6X1n34i4jXVx1MDAxYtss5zLyXHUwMDBmPkaE6bBcdTAwMTmvbVnMZ0Dy5pdGg/0yhtlr3adcdTAwMGZxOrmqwG4wK5GLz1x1MDAxYt5cdTAwMGZzVWpHKk5sZa4ynCfv0bMwVzGmt3swo1ZcdTAwMTiceDSZQF1cdTAwMDNcdTAwMGVcdTAwMTdcdTAwMTLDfi2pxKhg/NuXXHUwMDA04OVcdTAwMTBflPuRNmNhrs1oM6ZL/vCMjlx1MDAwZVx1MDAwMnWMK+A4hDqW9Xid0rmDc5/hKLtcbm9cdTAwMDAnw8VsxvTd2WKNsjWxXFzbXHUwMDFkXHUwMDE57JdcZlHB2ViTKDpcdTAwMTFA8ae2JsNcdTAwMTiQK20yXHUwMDEyXHUwMDExbH8yY+B9L4+RuMaXXHQjQZHZM1x1MDAxObXC3XaVX3tX8p7C42XuW/787C7dskWtdzOGXHUwMDExm1BcdTAwMTdGjqyNXHUwMDA0YVVNc1uCi1xu9/6qJSdsLz8xXHKo3u/LYaaZi4+srkVlofEt8j/aXFzk3Duvllx1MDAwZW/xoylcdTAwMGJaiymlTVx1MDAwNsNcdTAwMTJcdTAwMTLfjuvNXCLBqSOcVpIyW9XE7ZbwXHUwMDA02Ti8f1x1MDAwZqC5QFVcdTAwMTRMKrvNLUwphl+UpoY7dr9jw6xcIlxuPcFbyH5cdTAwMGLt14xxobiKnfLDW6CEXGJit8H4XHUwMDE0b7Ew1Wb0XHUwMDE207V+LZ7CwFHhxm6/ZPc+l3w8WWB3PVWKgTJcdTAwMDKHj6nFjMX0pVx1MDAxZSNuXHUwMDA3m4KjiZG60XbhsFx1MDAxZW+UcLTRXHUwMDAynaxcdTAwMTREYYipV9pbZJJcdTAwMTBsf8awm2Qtvry8/7rbbOYjRNzrcCCW/eKLQlx1MDAwZm5yvet7XHUwMDBmm5NJZnn25aU7rep4/SVL3798/z/RhT5iIn0= 901245673Input()Switch()Label()

    There are three types of built-in widget in the sketch, namely (Input, Label, and Switch). Rather than manage these as a single collection of widgets, we can arrange them into logical groups with compound widgets. This will make our app easier to work with.

    Try in Textual-web

    "},{"location":"guide/widgets/#identifying-components","title":"Identifying components","text":"

    We will divide this UI into three compound widgets:

    1. BitSwitch for a switch with a numeric label.
    2. ByteInput which contains 8 BitSwitch widgets.
    3. ByteEditor which contains a ByteInput and an Input to show the decimal value.

    This is not the only way we could implement our design with compound widgets. So why these three widgets? As a rule of thumb, a widget should handle one piece of data, which is why we have an independent widget for a bit, a byte, and the decimal value.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaVNcdTAwMWJLsv3uX+HwfJlcdTAwMTdx6VtZlbXdiFx1MDAxN1x1MDAxMyDAYEBcYiPWXHUwMDE3XHUwMDEzjpbUWkBcdTAwMWLqXHUwMDE2ICb831+WzKDW0kJcdTAwMGLybVx1MDAwMSYw9KKurjqZeTIrK+s/nz5//lx1MDAxMvXawZe/Pn9cdFx1MDAxZYt+vVbq+Fx1MDAwZl/+cMfvg05YazXpXHUwMDE07/9cdTAwMWS2up1i/8pqXHUwMDE0tcO//vyz4Xdug6hd94uBd19cdTAwMGK7fj2MuqVayyu2XHUwMDFhf9aioFx1MDAxMf7L/cz6jeB/261GKep4g4dsXHUwMDA0pVrU6vx6VlBcdTAwMGZcdTAwMWFBM1xu6dP/j/7+/Pk//Z+x1nWCYuQ3K/Wgf0P/VKyBio9cdTAwMWXNtpr9xlx1MDAwMkMtwGqAlytq4TY9L1xuSnS6TG1cdTAwMGVcdTAwMDZn3KEvT9XLm28n33vfOrlCrprJt2VcdTAwMTFLg8eWa/X6adSr95tV7LTCcKPqR8Xq4Iow6rRug4taKaq6XHUwMDE2jFx1MDAxY3+5N2xRT1xm7uq0upVqM1xiw6F7Wm2/WIt67iXZy8FfXHUwMDFk8dfnwZFH+ktZjzPNJVx1MDAwN8tBoVQvZ93tdNBDg6BcdTAwMTQ3hiGzI83KtOo0XHUwMDFh1Kx/sP7XoGFcdTAwMDW/eFuh1jVLg2uwqIKyXHUwMDFjXFzz8PyyXHUwMDEywWOCc8HBXGJgTL5cXFFccmqVauRcdTAwMWGiwZPGcC0541x1MDAxMjRcdTAwMGXGLlxm+oNcdTAwMDKSziM1cXC3e357v9RcdTAwMDfIv+Nd1iw9d1mzW69cdTAwMGaa7E7sxEA1uKfbLvm/xlx1MDAxZZTWRiDn2sKgXHL1WvN29OPqreLtXHUwMDAwLv2jP/9YXHUwMDAwp1x1MDAwNEZMXHUwMDA0KpeaXHUwMDAzXHUwMDA3XHUwMDEwM1x1MDAwM/VwL7NxcJcrlc+yqnfUqu9njoVcXFx1MDAxY6j8jYBKw/5cdTAwMWFStWeVRNRouZExXHUwMDAw9G9X6Fx1MDAxOSOBsGxBW7RmXHUwMDE5pEZcdTAwMWS/XHUwMDE5tv1cdTAwMGVcdTAwMDFhXHUwMDEyWtFTglx1MDAwYlx1MDAxNExcdTAwMDLAXHUwMDA0sFxuxTxcdTAwMDPMXCIqISRcdTAwMTiuxsDKjdBKk5Z5Z2DVXHUwMDEyXHUwMDEysVxuKNFqasrMWL3LXHUwMDE2dq5cdTAwMThuPcHm4VZWl0s3XHUwMDE3359cdTAwMTKwOoK3v1x1MDAxMaWSXHUwMDE5bYVS1oBRYyils6TMNKCyTOtcdTAwMTWiVHhcZpQkUaGnWW3GYcqNp1x1MDAxNFx1MDAxM2hcdTAwMTTjSlx1MDAxMKTHYGolkKDhOqrUoF6vtcOJXHUwMDE4VUYkYdRcdTAwMDJaxpTUM0P01lx1MDAwNPzkoFx1MDAwMPmbrd7RiVx0ermjy5NFIPpWXHUwMDE2/3WIaushXHUwMDExXHUwMDFjXHUwMDFhc6GsiGnK/u1cdTAwMWE9lFx1MDAxNmjotbJWxPpqXHUwMDExk1/2JXGLcXiCoDZwLq1cInvOkSz3XHUwMDA0m889qVx1MDAxNFpSoYxcdTAwMGLBUY/iXHUwMDEzJFx1MDAxMVx1MDAwMuBkXHUwMDAy31x1MDAxNUC10ElcdTAwMDBcdTAwMDVcdTAwMDKv5ZbPXHUwMDBl0KenzUaNXHUwMDFkXHUwMDFlbaszWVx1MDAwNX9v92t19yzdXHUwMDAwXHUwMDA1pjwynMqANcYy4GJcdTAwMDSiwuNkPi0zilx1MDAwYkviuiREXHUwMDBiRDlXXHUwMDA1UU28lIGAd4ZQm2jmOelcdTAwMTR6aTE7QkWb1W52oqPsTvXmQF1cdTAwMWbnXHUwMDFizcJWylx1MDAxMSrQXHUwMDEzXG40k2RnLfkvOIJQwlx1MDAwNYGT/Fx1MDAxOYtkXcUo/5hcdTAwMGahwFx1MDAwYsaoVSGUXHUwMDEzOrVUxM7WXHUwMDBmoq84TixcdKRgiXxLLYWcXHUwMDE5pfXNzon6epCv6G+t81x1MDAwNopM/qmeSzdKqas9TV6xXHUwMDAy8kaI5ckhkHJpyLcnT8pcdTAwMWHpPEi1XHUwMDE0Rsvl8iSATkBkrM//a8aJf1x1MDAxMvNcItdtXHUwMDBlXHUwMDA0/lx1MDAxN1x1MDAwYlx1MDAwMzSI5yM/k4H5cs/g7lx1MDAxOJqi4HHApGNDn+/ulvf38o9cdTAwMGbly1x1MDAxM7v1vWefXHUwMDBluzdfXq77+fzbb0P9UDtjgJc2XHTvhlx1MDAwNpuTzzl7mGDyO0+Ge9UvVrudIPWAV+LtXHUwMDAwP9X5ilx1MDAxMZaBtzVcdTAwMGV74thcdTAwMTaRpzRcYjBcdTAwMTjrVjM6rT31jTtcdTAwMWI6uus3avXe0HD1sUn989nGuy9cZuh5fcVrhq7crNcqzb52XHLKw5COakW//nK6USuV4kq+SFx1MDAwZvfpXHUwMDEzO/uz6OZWp1apNf16ftC2JazKlLCxZuQsky2dnfucibNN0auD3GiI7c7DztNcdTAwMDO/a6ZdzFx1MDAwNFwiiVx1MDAxOSlsziwoXHUwMDE0w6E4XHUwMDE0xrMo+yFcdTAwMDUgeK8uXHUwMDEyh8LTTFklXGaToKScXHUwMDE0i+PcU9pKbUnyXHJcdTAwMDNmx2NxKFx1MDAxNbXXmnnEcCHrk1x1MDAwZVrEWWL4XHUwMDAzhJRcdTAwMDaQ3P6ZXHUwMDAxfHC2KcOL6Nthpn2OXHUwMDA15V/dV6r3f/+8x1xmIFbSM8BcdTAwMTVcdTAwMTB7N9rYYVuByDyJZCVcYlrWWFx1MDAwMtdS7FxiWZFJPclQeIzcXFxcdTAwMTIoJS11PUxcdTAwMDAweORBanJ6ibxcdTAwMGIjzSh+OWlcdTAwMWOpXHUwMDE5zuNhrjV8XHUwMDEzWVx1MDAwZVjqSnLI2OzwbTX09fZ2ttjL3NbuNkVcdTAwMThcXFx1MDAxZUo/7fpXcu2Ri0lcdTAwMWVcdTAwMWYpNOByXHUwMDFjuoygXHUwMDA0XHUwMDFjOCNcdTAwMDeUL1x1MDAxNcH7XHUwMDFkXG6YMcmN4UZ/XHUwMDEwXHUwMDA0y+T5PMWJunJtZvdLg+2Tk30ondejnVx1MDAxZvLxurXFi9dmXHUwMDFkXHUwMDE0sETjcSFQgVx1MDAxMJprMCMoXHUwMDA2wo22StF1xDFSq37pXGZcdTAwMTNcdTAwMTaIQ3xcdTAwMTD0qsTgtJFSxVxy6WvQLdT3KmdcdTAwMDeb96fFQGm1n+ncdjN3aVe+XHUwMDAwzCNqa1x1MDAxOTjSKKRcdTAwMWVcdTAwMGVOo7BcdTAwMWVjKDRKXHUwMDAxXHUwMDFh5OqczDdSvmQwrTBcXHxcdTAwMTD4XG6ZXHUwMDE4XHUwMDE0JFx1MDAxMWdO+9qZXHUwMDAxvHt4e3pZzp0/tJqN/ePK9SPb7lx1MDAxZa+D7nUgNlx1MDAwNFFitprQg6PK17lwmoNVTFxuS65cdTAwMWMug+J/XHUwMDE4ZVx1MDAwMjshfv1cdTAwMTbql8yHm1x1MDAwMjRcdTAwMWaE/VxuTI4+SO2yo+RcdTAwMWNJa2fnu5eH29vlXHUwMDAzfVx1MDAxNlRcIlZu4o/N9CtgRbzBcMVcdTAwMTjXqK1cdTAwMWVGLpFfXHUwMDEwXHUwMDFjmJFutmOFXHRcdTAwMTZvo35dhlx1MDAxMCNcdTAwMDb/QbQvxnyVUfKgLLWC4eza965Z7+BBuVuA4kE2f3GQffpRTprZTpf2VS7zhli+1eSxas3GMcystYJ4XHUwMDA0MSrUy8VcdTAwMWVWqX1dviZIXHUwMDBlXHUwMDFmxHNDlpzdpvv5fHaOXHUwMDE5xd5pMTRFznd2s51K5nLv5OvGblLKcGq0L+fWI8IghZtHUVxc4Fxidok5SKZcdTAwMTDRzX2DhKWYw+/gv1x1MDAwMP08zY+igE1y7EGgy5+Nx9deQzDkq6adLVx1MDAxN24u9+5cdTAwMWHb19nKg9pPQdL7bCgmssQ5aWF0UbIxXHUwMDEwI1x1MDAxZFdSo1Nvy2VvrFL/KlwiOvTNPlxi+0WTXHUwMDFj+yW/jaivkLOjt3rQyYeNw/bOWXi3u5FcdTAwMGZt8VtcdTAwMTZTr3+N9lxiXHUwMDEw5LZcdFx1MDAxN26B8ehcdTAwMDO4XHUwMDE4XGZotDruLKRU+yrSOZqA9EHUr9TJXHUwMDA0wjDUXHUwMDEwp1Kv4fe+yO5OK7WwsbeFm9cnd5FfbZXXQvtcdTAwMWFDXHUwMDA0V1x1MDAxYmktM1x1MDAxNuOZXHUwMDE4LyBcdTAwMTY0NuS/uUVYfLnMpJXyX+E+wX6U2K9MzpyHflx1MDAxN7mMlpnxe+37aifb/da5rCt21oyAIT9Ku/5Fhlx1MDAxZVfoXCKmjjnwUeZgPcFcdNZklC1PfeqDXHUwMDEyaDR5mlx1MDAxZlx1MDAwNL5KJUZcdTAwMWaASa5cdTAwMTXOXHUwMDEzfrjZle3vR+qSseuef3WV2z82lfN1UL9cdTAwMGXC5KGRLTJWS8PNuPrlVmtcIsdcdTAwMDC4ZHL9SpWvZkyTz/1RtK/iidqXM6Vccqp4mspr8O12evuHXHUwMDE13Fx1MDAxMVet4F5vZXgzb3qp176qr32ZtVx1MDAxY7hcdTAwMTAxr/1F+2olhVx1MDAxNkwxxLRcdTAwMDd/wdL7WMnYXHUwMDA3XHSfUZcl6l9LbJBjXFzdvFx1MDAwNuDHvdtKeHPh8+vHXFw1v3lzXHUwMDFmXT9l10L/9leBgjbKXG5Ffc/lOIpcdTAwMTGFW+5O/2C5OYzVTr4xg1x1MDAwMqx8P5mTSan1XHUwMDAwidDlXGbBXHUwMDE5ojnm3W7bX4PM1VnQ/V45yD49Ni/CcmEt5o0lck9cdTAwMGLnsVx1MDAxMWqJXHUwMDAxj0RcdTAwMWaAk/7lXGKSVLNEmMJcdTAwMWRcdTAwMDRcblx1MDAxZlx1MDAxN0RubLRcdTAwMDdQZaPYXHUwMDA06Vx1MDAwMtBcdTAwMTg3mqvMrid/NVx1MDAwNpHFsutjXHUwMDFm8Ep2/VDPXHKS63HowlmT66NWOymzfuhcdTAwMDVG0+jZ9Cz6JFHSyYtbXHUwMDA10VFrxFx1MDAxY9lvW6J9b3PhUfugoDG30WlcdTAwMWSZncw6SFx1MDAxMlx1MDAxYfQss8LNYVx1MDAwYsVwNInTZVx1MDAxOXEpXWcoMlx1MDAxMlOWYS8jSpNoy7gkUWPcYtx5WMq6XGJcdTAwMTKkQZBgMUFcdTAwMDKZKEnkyoJi8VmtV1NB7ptf4ewqd1x1MDAwMUenbV3M5i+KrcZaXGJcdTAwMTKxXHUwMDE0cNEnRiBlio+LkbHOPLtcXDzF/1aLhIKcXHUwMDEyXHUwMDFiX4L7flx1MDAwNImnQZD4goKkk1x1MDAxN1xuK7DkW1x1MDAwM5+d3PXC6sPj7db53fX33Yvz5tXF1d7xWqSlXGJcdTAwMDaelsI4v1x1MDAwNInLjYpcdTAwMTLJmZGMPG9cdTAwMTJcdTAwMDNjkr2SpSRpklx1MDAxYjIuSeSSXHUwMDE4l1x1MDAxZP5cdTAwMWW5XHUwMDFkpkGScEFJSk4vUORcdTAwMGVYXHUwMDA1c1QuMVdcdTAwMDf4taDOiz/OXHUwMDFh55nocKdcXM111kGQOCrPOs+USVdcdTAwMDVCglx1MDAxYZMkblx1MDAwMazi2lx1MDAxNV5YlZs0kyhx5Vx1MDAxMu90LMfs/UiSTIMkyUXJnUmSJFJ+Uiszz1wiocvDo4ctc7b740T29i5yZYjqtbVcdTAwMTAlsjVcdTAwMWVcdTAwMTlcdTAwMWNcdTAwMDZE4MhcdTAwMWRcdTAwMDJtxkTJoGJcdTAwMDY12Wlr2GrcpFx1MDAxOa1cdTAwMTJcdTAwMTlPRoZxrnDYusiSSoMsqVx1MDAwNWVcdJP5XHUwMDFk00R7mJhjyX6mlM1sh+a8cZMrXHUwMDA03ZNWbvPworJcdTAwMTayxJXHXFzNLGt5v0TTqCwxz0pE1NZVfF2VKLFcdTAwMTlFXHRcdTAwMTmq91x1MDAxObzTaVx1MDAxMCW9mChxlrz+RLmiySDZ7GZcdH50wzB6aLDj8z14ZFmf5UpJ1S9SJUpCcY9cdTAwMTO3XHUwMDAzMJqTXHUwMDBlMSPRO2Y90iuuQFx1MDAxZFx00rRVKKuPOmhUXHUwMDFhLYjfXHUwMDEzdEAyw/DbJEmkQZLEXHUwMDEyxWRscja2XHUwMDE0ynKNZnbD1Ny/vTu4lFulzrm82snrS773mH/jXHUwMDE5/ZJcdTAwMWZWg1x1MDAwNFFiXHUwMDBiiZJcdTAwMDTjITdMMlx1MDAxN1x1MDAwZZdsJOrAhMcsXG6ppNJkqKfU4Sha7nN/qihNndN3XHUwMDE1pN1cdTAwMDNcdTAwMTQ3rmrNpNJlWnmayKawQjBqT9xxe/amXHUwMDE4J1x1MDAwZahQvZ3dej4xUrLsOdRkbOPh4TJ7XFyyt5uV3Yub3o5ccl/ebFxihn6n03pIYc0ySF6PS+4zMVx1MDAwMFx1MDAwMDV7XHUwMDE0+yravc0yntssfisp2/leOdkpJk1cdTAwMDelR1x1MDAwNJRcdTAwMTSeXHUwMDA1REt9bp1cdTAwMWIzJFx1MDAwMsJcdTAwMTJt45qTOpBcdTAwMTIlT15Rs6xcdTAwMDRALD9jSt0yJVG4+mlvZ1HWXHUwMDA345Ps1exF0bZq0elDjVx1MDAxOMw//2ey5VqsOtqilivenKnC+6tjJ0hvPFx1MDAwM2tsa1x1MDAwMtMvkzBHStr0oU5pSppC6TF62X6VXHUwMDEylzI5Yr9cXD14QSBcdTAwMTP0rfiUelx1MDAxMEvbL+Zxi1x1MDAxMo0w9CBcdTAwMTNPm1x1MDAxZlxis/Ikama50UowXHUwMDFlr/k6qMVJhpjFd5RIZVHCMPI70VatWao1K1+Gsoqet4XZn8Eg9CW22O1rbk+7jG3JXHUwMDE5aMuJdsjYRVx1MDAxNb9Nl1x1MDAxOI+4LVxyN/F+RaOtn89cdTAwMGbSloJm6fVcdTAwMTZNZ2lDLVx1MDAwMueZ0oOk1Vx1MDAxY21sqeVLk8Ajb11Zolx1MDAxY9KQbVx1MDAwMDueS1X3wyjTajRqXHUwMDEx9XyuVWtGoz3c78pNJ+LVwFx1MDAxZlNcdTAwMWH0UvFzo7qg7T5xWIlcdTAwMGZ++zyQlf5cdTAwMWYvv//7j4lXbyRj2H2No3fwgZ/i/y9W2FFPKSymjVuKPUf91Ivi9cHTRifs7FfK/KZ519RVm5RcdTAwMWSeXHUwMDFlJmKkJ1x1MDAxNaGNSctcdTAwMTSPr2v5xUS0p6VcdTAwMDRgVkvNpuSkXHUwMDAwKUO/sLguI5XpKdRu6p5+XGJpJySqXHUwMDE4XCLj5NhKrV1cdTAwMDaeVLFuet69QjO0Kp4/uUqeclx1MDAxNvpPcHxRr1x1MDAwNyivRKF1nrHifM24uJ1qzSWNylx1MDAxY8vbj/Ld7t2PTDPfXHQvXHUwMDFl7cVcdTAwMTM8QEGnXlx1MDAwMiR6iEBcdTAwMDZbWaflcZiLS4PkXHUwMDAxXHUwMDEy+pnb1YdjMlx1MDAxN19WXHUwMDAyJm4sNKGGsHSRUivecCZ8fUC+JFx1MDAxOe9FwX6z3Y1SQsZjzVmMjHNIXFzexFx00W5XojnSWKZcdTAwMGZ1Ssk4MOOJX8XXmDDkTVx1MDAwZlenkFp5XHUwMDE2hVx1MDAwMSO1ZdM2XHRbVnyJqlCPc86MsYbHp/hfhNlcdTAwMTJ5c3tHWGO4dcHkMdnWxEJcdTAwMTU19H1Q8en2IEZ8N4j5up3C0FxyoUFr9ThcdTAwMTfXnuHSXG43TSU1qlx1MDAxNyY4J1x1MDAxOZ/O0obdXHUwMDAz0vi/Qn9cdTAwMTJVrNrugIuvNfVOXHUwMDA0rPvaXHUwMDE4w+qbXHUwMDEyb56Y6kBcdTAwMWTNNeBcdTAwMWMxhLMrv36+fdPbPtBfe49cdTAwMGa7ufvd3lsvKp7GOlx1MDAxNtySS3jo0qxcdFmCSzWyuSFnrqKv1lx1MDAwMiXTgvh3Mu2QRYustFx1MDAwNPFcdTAwMTbcI34vgLqd+t5MmGJcdTAwMTJgPCW0KzFM9IjJ8Z04ibZcdTAwMGJt4uuaVslJXG7lw9ZtXHUwMDA3mtunnfLpcclcdTAwMDS3N42t9Vwi3skxcCBcdTAwMTJKqnCeXHUwMDE4+OZjq7jTuS40N+Do/vvtzs2VLrdTL1x1MDAwMZxxr79lXHUwMDAwMuVKQlxme56ESE/2tylSbqNcIm2SXHUwMDBim/CC8UWwXHUwMDA08eaTplUnXHUwMDEwb7fRXHUwMDE1zLVpwHvB+PK8e6e/jXN6iPdLe1x1MDAxNmPekFxck95tXHUwMDFlIIycI+11+linlHiTufBcdTAwMTi3RJtcdTAwMTVze9uO7NiHzPFcdTAwMDdcdTAwMGJCMUcweHJCxNLia0hRKCPIXFxcdTAwMTJNIco2gXlL44JLxlx1MDAwNbpcdTAwMTUqsmJcdTAwMTOkW1omh2pcdTAwMDGvM/Weblx1MDAxMoapN2Ouklx1MDAxNtFu5crzgIs7j1Nd8MiddDXPXHUwMDA1uVOAOFx1MDAxZXaeiX5PJ2tD9Jsxji40LJjWNGh2kFx1MDAxZftOXGJ4MnDd1zhkk1x1MDAxOPin5yd88dvt04iA9tL/XHUwMDA04VrpWV1cdTAwMGZe88t9LXjYmryjm9vU7dNzhzrFXHUwMDEz9Fx1MDAxN3D//PTz/1x1MDAwMcOHY8gifQ== 901245673BitSwitch()ByteInput()ByteEditor()

    In the following code we will implement the three widgets. There will be no functionality yet, but it should look like our design.

    byte01.pyOutput byte01.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258abyte\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Note the compose() methods of each of the widgets.

    • The BitSwitch yields a Label which displays the bit number, and a Switch control for that bit. The default CSS for BitSwitch aligns its children vertically, and sets the label's text-align to center.

    • The ByteInput yields 8 BitSwitch widgets and arranges them horizontally. It also adds a focus-within style in its CSS to draw an accent border when any of the switches are focused.

    • The ByteEditor yields a ByteInput and an Input control. The default CSS stacks the two controls on top of each other to divide the screen into two parts.

    With these three widgets, the DOM for our app will look like this:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1daVPjSNL+3r+CYL/0Row1WXfVREy8wdnc0OZo4O1ccsLYXHUwMDAyXHUwMDBijG1kmWtj/vtmXHUwMDE5XHUwMDFhSdZh2fiiPUPTLclyuiqfzCezslL//bKwsFx1MDAxODy33cW/XHUwMDE2XHUwMDE23adqpeHV/Mrj4lx1MDAxZvb4g+t3vFZcdTAwMTNP0d6/O62uX+1dWVx1MDAwZoJ2568//7yr+Ldu0G5Uqq7z4HW6lUYn6Na8llNt3f3pXHUwMDA17l3n/+yfe5U79+92665cdTAwMTb4TvghJbfmXHUwMDA1Lf/1s9yGe+c2g1x1MDAwZd79//HfXHUwMDBiXHUwMDBi/+39XHUwMDE5kc53q0Gled1we2/onVxuXHUwMDA1VET0XHUwMDFm3Ws1e8JSXHUwMDBlVFx1MDAxOcPDXHUwMDBivM4qflxc4Nbw7Fx1MDAxNYrshmfsocUr9nK7d9N9rJtm6eB43Vx1MDAwZs5L5+vhp155jcZh8Nx4XHUwMDFkiUq13vUjMnVcdTAwMDK/dev+8GpB/dfARY6/v6/TwkFcYt/lt7rX9abbsd+fvFx1MDAxZm21K1UveLbHXHUwMDAw3o++XHUwMDBlwl9cdTAwMGLhkSd7hWFcdTAwMGVQQY0xRIDR6v1s7/1cblx1MDAxY6GZltQoKpRhfXKttFx1MDAxYThcdTAwMTMo17/EXHUwMDE1q1V5KNllpXp7jeI1a+/XXHUwMDA0fqXZaVd8nK/wuse3b0xEONB117uuXHUwMDA3eFCa8PPc3rhcdTAwMTOliFx1MDAwNk01fz9jP6W9WeupwH+iI9OsvY1Ms9tohILZXHUwMDEza/1qXHUwMDEzVZ2Y+lx1MDAwNO5TKG1ksluVdndcdTAwMWJMUN7erizz48btaaVZXny/7p8/0m/7+ubO3vdye31cdTAwMWb2dlx1MDAxZb7V+MWe/H7sPsU/5dfnV3y/9Vj0vjv+wVx1MDAwZn61U/7O+ePekeeXvNXvlWL3fftbOIDddq3yqutEKlxyXFyDUSZcdTAwMDKXhte87Vx1MDAxZttGq3pcdTAwMWLC40tE4Fx1MDAwNCxj41x1MDAxYUGkzEakXHUwMDAwbrSRQlx1MDAxNkZk+iSNhEgyNURSMI6IIDJcdTAwMDKC3lxyXGadXHUwMDE2JClNQpKKXHUwMDA0JKlihlx1MDAxMEXk2CCZoYVKXHUwMDBiqUBLoYbQwnC2W83g0Hvp2XaIXHUwMDFkXa/ceY3n2IT11Fx1MDAxM1x1MDAwN2j5OXDXev7m67+jI9lx8ZPtrYiOvWep4V1bNV6s4ndx/ZiGXHUwMDA3XHUwMDFlurD3XHUwMDBi7rxaLeqUqihIXHUwMDA17+lvXHUwMDE2cSYt37v2mpXGUZqcucB7XHUwMDA1flxu8lxiRIal31x1MDAxOVx1MDAxMqI1IyxyxSDs5du4ecWe0Fx1MDAwZYBUgilKXHUwMDE1NSBj2KNcXDqCXHUwMDE4oVx1MDAwNbUjolQ2+KD3XHUwMDFhXHUwMDFkfEY5QDgxXHUwMDA0lOT4k0SiXHUwMDEwXHUwMDBl2lx1MDAwN4WAXHUwMDEwXHUwMDFhgCnSXHUwMDBmTHtKgpHDuMrQqfxSXHUwMDE4+nbknynDtVx1MDAxM1T8YNlr1rzmdVxcsDfOV1x1MDAwNCf2O1fa1q04hlx0nFPKcUg0ZyxyxVWr2rXfo1x1MDAwNFx1MDAwZcd5XHUwMDE30nAj0c1IymTiy7vN2mChLq7u/aP9l2Zru37hi43qQ1N8f0xcbmVcdTAwMWOCdp5cdTAwMWHG0KArSXm6UIJcdTAwMDHHadRIzigorlx1MDAxMjI1Kp1gpXV351x1MDAwNTj2XHUwMDA3La9cdTAwMTn0j3FvMJcs8utupdZ/XHUwMDE2v1P0XFy/iWjbO8apUvi3hVx1MDAxMEG9f7z//T9/pF5dylJs+0qodHi3L9HfWbYtl+tTKmX/4Xf7XHUwMDA2SC6oXHUwMDA2XHUwMDE1erxB9i1/jmdi39Qg86aII1x1MDAwNdVUcEMojm+c7DNcdTAwMDKOMlx1MDAwMq1cdTAwMWLHWUDjJ/rkXHUwMDFhn3UjOiQ171x1MDAwNi1cIv4vXHUwMDBihryTcsZDXHUwMDFmO1x1MDAxYrJ/6W8/updcdTAwMWKb9eP6snfmnrfXl1fOZk32L7rPOytwt/tcdTAwMWPcd8pAXHUwMDFloHJTL81zXHUwMDEwMZI/XHUwMDE4KYigVOssqGtmJEFcdTAwMTdQnMmkz/6cI13mI11MXHLpTCWRLpNIXHUwMDA3gaKCoWND+qRjiEi4MyCGWPnF7L/+bNpcdTAwMTNV9NdcdTAwMWS38/fPxaDV/rn4s5lcdTAwMWVZXGJcdTAwMWW703vg0HCvgtHjilx1MDAwMW6rP64oIPtcdTAwMDc8slxmeVg/TFx0Rr+AlEdcdTAwMTfH6crhnqqdbHzb89ae2lxcb7i+u+TNe/6NXHUwMDEx9MlcdTAwMThYIf9AXHUwMDAyQkGyXHUwMDE4UjlnjqaMIDUhXHUwMDFjgOpMoH442jeQXHUwMDA0qkpE+5Io4JJFvtdsXFzyzfpccl8+u1/deKhcXH6/a+12N48vr4q6uJuLk12x3azed4+f6jdUXHUwMDFk3d22XHUwMDFlxuA6XHUwMDBmbsr153OvfHrbeVhT61stf3XDXHUwMDFiw323r6ube+tcdTAwMGZPasv9UTnaa+/drWP8MS6XzFx1MDAxNGOhXk3KJUtNM7HOuOGaUiiO9fTpn6lPLoB1XHS5WEd6zlwiWFdcdTAwMTPDuklL7CV8sjXBlKE846Pf8+OUbcJss9nuXHUwMDA2WXm9XGbv+9G83lx1MDAwMCeVltf7JebojpYh581cdTAwMDKfMMC1UkNk9nboXHUwMDBm/2bHXdl9WuvSMrku7Z+Wm/PuZylTXHUwMDBlXHUwMDE4xJbgXHUwMDFh/zPxxJ5cdTAwMDTiXGKNJopcdTAwMTjFpFx1MDAxMlx1MDAxM3SzJMXNykTujjD0sdJwOevQl2JQeVjdO395emr5vPHy/HJ/dTRrP3tx841cdTAwMWVtPK2xTX/l4rl+u+5cdTAwMDdwPob78v3DXHUwMDA3s7NcdTAwMTI86lx1MDAxMu8snWnXK2k1Lj+rqWZcIlT4XHT5WcZ19pI2hnpcbqO+SEZ4XHUwMDEw1tOnf879LOU8XHUwMDBm65Q6MFx1MDAxNayblKx90s1SQimyXHUwMDAxXHUwMDBl41vSXHUwMDFlp1x1MDAxNn7QzXrB4aNcdTAwMTdU619hun52gJNK+NmInFx1MDAwYlx1MDAxZlx0aVx1MDAxNWR6WoywMJzT0lx1MDAxNF/AfvK8yr1/e3dSWq+wXHUwMDE3ft899PZmvIgmXHUwMDA3pp7AXHUwMDExxiiOP4wzwXhcZn2cI8lVlFx1MDAwMVd4XHRcdTAwMDOSzXI/nHoyPIk/naS5XHUwMDA06ZEydIy5p9FcXO3a/snN7qM8XT2g66XT1WD7eUOuTSFcdTAwMWI82HVNKWurc7CjqUKFkUOkg9KHc86xI3KxI+LY6WfP41xcnyFJ6CSztkhmmcJIVY4vXHUwMDFiND9cdTAwMTHia9j1mvbslSrWW42a6//9c/FcdTAwMTKDss60XHUwMDEzt1x1MDAwM1xcQb9DKyR9LkwzXHUwMDBiRVx1MDAxNCGZMLWL2ZrpyIrdwHXUXFzDNadcZpNI5lx1MDAxMFsqXHUwMDAwXHUwMDA2tZ/RXHUwMDE4TJm2oSaA0JZeXG5cdTAwMWVZdlx1MDAxZTdOwaFcdTAwMTRxITV+XHUwMDE0YsPoXHUwMDE0jyeYXHUwMDAzQjFcdTAwMDCNYT5cdTAwMThcdTAwMTFBwHuih4PNtqthKrg+XaVIwaJcZulQw7mixtbgXHRJZFpVXHUwMDA2OFx1MDAxOEpJW4kqUG6NLMYkvnyhSpF8VL9cdTAwMGJFcJ654UJcdTAwMTBbmWKkyKhfXHUwMDAxblBcdTAwMWKkoTjjSlFJXHUwMDEyUn2qWpFs9bavpGKHN/xcdTAwMTL9PbSFo1xcZK5cdTAwMWZcdTAwMTODNIUoPVx1MDAwNIvPT4PMqYljWjvSXHUwMDE45F2ao1wi0ThcdTAwMTVBaDiK4yBQsGUz0ULtcVx1MDAxN8JRXHUwMDA3yZ8gqM5MUqHCiVx0iVx0d4iU1GhGgWiTXFy00ohcdTAwMGIzY/uGcpFIiD9++5afWY7bXHRcdTAwMDaaIZxcdTAwMTjYKmI0/ilW0NixJoxwUFx1MDAxY936aOYtP1xuj8uEQ8RcYrJeXCJ6dc9JmexKqVwi1iRTYVx1MDAxZFx1MDAxNv/cxi1Tse0rodJDmrb8JFx1MDAwNc+u9MVcdTAwMTkgXHUwMDE0lVx1MDAxNYrnXGJ3L1a+lcs3zD+puZetnSXxtNahc15cdTAwMWbDwDjoVDRDXHUwMDAzJ1x1MDAwNbKPOIMj3DGMgFx1MDAxMpzgXHUwMDBmzsLkXCKtVINcdTAwMTZJirwlXHRcdTAwMTFcdTAwMTNcdTAwMWMjv1kvXHUwMDA3bJ2s3DWvK8es/qw3ZGX95P6YXHUwMDA2s15cdTAwMWX/XHUwMDFkKtY4z2RcdTAwMWNI5jgyXHUwMDEyXHUwMDE4XCL3kT5Nc1x1MDAwZUkh8iDJtMOnXHUwMDAzSV0obY9cdTAwMDabXHUwMDAzj/Ki3yf1XHUwMDExVn1NN2k/wJNkXHUwMDE3p4285yW6kbBcdTAwMGZ2XHUwMDFj41x1MDAwZWLZbWHU5du3OeX5XHUwMDFjtFx1MDAwM5pcYs21pHh5nOczbVx1MDAxYyEw1jWogISTSEZy/LlcZoZ2TuGAI8pcdOEqJVx1MDAwNckxOqfCbv/UWuNcdTAwMTU6uVx1MDAxYk1cdTAwMGKD0IzuZfpccnNcdTAwMTn5QInzalx1MDAwMkJyYFx1MDAwMlVcdTAwMTlcdTAwMDNpmuTVysGJ14ZzrnrUO5k0KMT1XHUwMDBix1x1MDAxZuBgyI7ScMlQ+YxkJpbOeFx1MDAxNVxuldIuliE1XHUwMDA2IGj4PzfXz9Zt+0pq9ZBsP9u+oYPKsm+Ca/u/LJ6qzedZc2rfcMxcdTAwMWSltVwiXHUwMDFjObTk0X3mr1v6LOtAdkVcdTAwMDBpXHUwMDA3V5NcXFSh1JGglOQ2KWzT5CnMn6HeS6KYZFRcdTAwMTiI7C59s29cXKJcdTAwMWVcdTAwMTk6VLHApzNvhTf1UVx1MDAwMTiQhNg9myotJ1xuXHUwMDBlx1x1MDAxM1xubZvhQFxig1x1MDAxMXf0XHI0uL8kslx1MDAxYslcdTAwMDBQjVx1MDAxOLHqlCqSoIg80au8xVA8ZZPhZ7Jt2VptX1x0fVx1MDAxZdKy5Zc1yki7ikSaXHUwMDE2Z5zhNFx1MDAxNN/Rx5fXuydrm62V04p7UX95MK2no/05XHUwMDBmmohgjpBMqN6ym2AsXHUwMDFlNSlGXHUwMDFjXHUwMDA13O5lRf+OqjY541asrlEjhVRy9u076lx1MDAwZlx1MDAxYvewXq67bOX2qnbaofWDy4PfvfwwXHUwMDFhNE+q/FBFdnb2QVx1MDAxMqmcXHUwMDEyisjiXHUwMDAxVfoszTlcIqXMRaSgXHUwMDBlnVxuXCLT1oKTaVxmLa2QZjo1/kPrYDjVo6QxdiqXbuPrz0X4ufjvhakmMlx1MDAwNriS/kRGXFzQXHUwMDBmOEStMkszKJJcdTAwMWZi2DBcdTAwMWLqXHUwMDBlzrfcs+f7Yyit7m6elPeuyrf1kzmHXHUwMDFm1cg4rLZJXHRUXHRcdTAwMTJcdTAwMDdcdTAwMWa6Q0OQiFCMXHUwMDAzlJi5N1Q4I2Cj4Fx1MDAxOXvD/bVcdTAwMDefL61cdTAwMWZcdTAwMWRstq+6Tb3JO+V9PVx1MDAxZl7LkGhcdTAwMTOWSXktozKz78hsXHUwMDE1V2irXHUwMDBiwyZ9NOdcdTAwMWI2XGYsj8yCXHL6LD5cctjIlK5cdTAwMTApLsvG8kzy6dTLXHUwMDBmq4BcdTAwMWZzWW9F6FN2V1x1MDAwM1xmfb+7XG6FzEVbdlbKiEw3hbGxJU5miEYsuWx8XrNSXG5pIJpcdTAwMTZGXHUwMDE1XHUwMDExOtqAoVcnrLjDNZ5lXHUwMDAwljH3yzU+xFx06ShJXGIyM2X7f6mUjaE2O8mRI1wiU1x1MDAwNIPxNEvm3Fx1MDAxNcG36qhlmEF9jaJ6tI0tXHUwMDA1k1LD1LIwu5hiXHUwMDA3lnDbtimSXHUwMDEyXHRrWVxmgM1cXDGMXHUwMDA2XHUwMDAwVT7x5Vx1MDAwYuWl8olmXFwornBcdTAwMTJccqBMVEguaVIocIxdf+VS4JByopNCfabMVClTue0rqdbh/b5Ef49QPZhTXsOBMsX0XHUwMDEwW93z+dW82jejUd9Q4yjYoSe0b1x1MDAwZp7CIJlcdTAwMTnCJVx1MDAxOE3BTM7AcVxmXHUwMDA3mN1Qj26FoUFlKVEx5460+1x1MDAwNFx1MDAxNcFYWEqW4OnUNiuj3JjZXHUwMDE2XHUwMDEwXCJcdTAwMTOZZNa9sIGzffKQMFx1MDAxYZtXt/tyeKxz3ZstYY5cdTAwMTFcYitbISGYXHUwMDFk99FcZlxcPjWJXHQlXGJcdTAwMTFcdTAwMDS0XaCgXGIxkpRcdFx1MDAxY2XLqI0t39WA4n1q+5at2r2z/Uo9pHlcdTAwMWKQeYfsblx1MDAxZb2F5KF6hZ7cXHUwMDFmXFysLYlD+dgodVx1MDAwZk42Lk7rL4ejmbjpdVx1MDAxNEDn4dhcdTAwMGVcdTAwMTmcXHUwMDBiTanS8ZhcdFx1MDAwN8hRXGZcdTAwMDRcdTAwMDBnyI50dtA0rY5cdTAwMDJcdTAwMTKhoW2XlbGFTe9cdTAwMWE1VLKhc7ZVW/Jqa+Z4/9ErLZeO5OXZ9lx1MDAxNDrh5N634ZLzXHUwMDFk0Tl9MFtcdTAwMDcvXHUwMDE1v96+WSndjeG+3aPmcX1rp7y0XHUwMDEx1O9fSof+j63zsW3L1EyISMg8qeSINJlduoSxLbmH6VxunDr5c05muGF5SKfMoVNBetp+5pS2PcCR3JNxds1cdTAwMWOnXHUwMDBlhnP9sX5cdTAwMDJquqWJXHUwMDAzXFxUdj9cdTAwMDE1cpqEyuwulkTbsihFSPE8Sb7pnFfoMeIwRjFcdTAwMWVlxvbcVvE4QlDuYEBLMNzoXHUwMDE5oslVJ2JcdTAwMDTg2MpcdTAwMGWjgCkqIa2Fllx1MDAwMlx1MDAwNykos5V0XHUwMDFhf6KlRG9xXHUwMDA0Tlx1MDAxYTBOh+rqM/Y4QlxiSkfKaI57I1x1MDAxMjhUXCJZx+jbprq0MskwQjjEXHUwMDFlN4RcdG2f/ZBk7IWiiHz0xqNcYklt/Vx1MDAwZVx1MDAxN7ZcZl0xlVx1MDAxMEk7XHUwMDEyY1x1MDAxZmVcdTAwMWK5aUWJTM7GZ1xuXCKy9dq+XHUwMDEyXHUwMDFhPdYgQqvMVkVWJ1RsZ/Ug81Zb3VIrRy+Px7v3z3urm9v+9f7jjDtcdTAwMTVcclxcdeFUOVxmXGZD48VRoYyMW7de9Vx1MDAwZdo1MDaLRcRcdTAwMDStW7FcdTAwMTiCXG6NTidatDCbXHUwMDEwgtzfXHUwMDFjlLdWa+X25Vn1uHPaZJuXT7831Udzwya+XHUwMDBiXHTNWnZBXHUwMDFkKG2fVjBE5jJ9muZcdTAwMWOSnDg6XHUwMDA3krZ8ZyqQTOvAkkL2kVx1MDAxYtk1hOl0YFx1MDAxOVpcdTAwMGI/RvZ/lcWoqdfvXGZwJln1O+qj9TsmO9TWUvPeWlFh+Fx1MDAxZEO9Jsjyj4fjhzOoPnWPbneuunNcdTAwMGU/ZF9cdTAwMGUyfeRV1G5cdTAwMDeH/vpcdTAwMWS7tIaUXHUwMDEx9dDk9Medkju0JVx1MDAxMchcdTAwMTWVnnVKbf3+XHUwMDA2bpmskcf79o/d5TX329Nz4c5hXHUwMDEz9Vu2gymfuN+yj8fJ9FtcdTAwMTTokHVv6cM558DR1NHZwFx1MDAxMeDoKVx1MDAwMKdYXHUwMDAxXHUwMDBmwUBdXHUwMDE5JulUMlRDq+DHnNZsKnhcdTAwMDbY+vFX8GSXeWtkioxcdTAwMDEp3lx1MDAxZiefkc9pZsp2wLG8XHUwMDBiXHUwMDE0aIK0MN5cdTAwMDNMKoaAY1x1MDAxYc+B4Vx1MDAxYbKbzH54gdtcdTAwMTJSYXu54MdQalJcdTAwMTDIpW0mQoxcIr0nN5DEU1W17fIuh2s2Pe60lEGNXHUwMDE56jFcdTAwMDSRXHUwMDExLZSWKpxcdTAwMDOybaeEwbhcXFx1MDAwMtJdXCIjnfhcdTAwMTdcIkvJtm8oKIlRkaIjPykun2jGheJ2kLTtXGbJKfL+lDV34iiGsVx0Z0Joxpn55Fx1MDAxZHIyVdu+XHUwMDEySlx1MDAxZN7uS/T38Gl3XHUwMDE2cZ79xcD24YHUXGaxZzafXc2rbVx1MDAxM9ox9slrRCvOXHUwMDE19Fx1MDAxN+9oRzImeoVcdTAwMWaCkcl14kBcdTAwMDVcdTAwMDD7uC6tKUFcdTAwMWIlU9a/mHGQ/YHtQsqZ3Vx1MDAxY9tv26Tdi4k2epZcdTAwMWJmR6cg47ZtNn6SYG2IsrUhXHUwMDEwZmBDK8JcdTAwMWRj60okXHUwMDA2o4xSKZJcdTAwMGacLGTa8ilJXFwmxlx0t9V46E2FTjFsXHUwMDE4XHUwMDE0okhUo1x1MDAwMUT+ysTnzrlnqrV9JVx1MDAxNHpIu5ZcdTAwMTUjXHSZmWy3T3lcdTAwMTmi39fz877ZbW3uXpOT/c7SycqZ9+1ged5NXHUwMDFhU8ThvTJPaqjdnFx1MDAxZDdpgCaNgCUhhGnOJ/dQbZPymHuRiJC4ffpcdTAwMTmHKVx1MDAwNUh2q/pIzGukR2o7jvOz+ZUsXHUwMDA0db+7INNX8SNcdTAwMTMwXFyYXHUwMDE0tNpZMVLsXHUwMDBi9Vx1MDAwN0T9Qr1C7MtcdTAwMWKEXHUwMDE3K+32YYCj9m7ucD682ttXXHUwMDBmb7z44LmPy0mN+NdV72Xv2oOtxYjbczT/fPnnf+6uXHUwMDA3ViJ9 ByteEditor()Container( classes=\"top\")ByteInput()BitSwitch(0)Input( placeholder=\"bytes\")Container()Label(\"0\") Switch() BitSwitch(7)Label(\"7\") Switch() ...(1 thru 6)

    Now that we have the design in place, we can implement the behavior.

    "},{"location":"guide/widgets/#data-flow","title":"Data flow","text":"

    We want to ensure that our widgets are re-usable, which we can do by following the guideline of \"attributes down, messages up\". This means that a widget can update a child by setting its attributes or calling its methods, but widgets should only ever send messages to their parent (or other ancestors).

    Info

    This pattern of only setting attributes in one direction and using messages for the opposite direction is known as uni-directional data flow.

    In practice, this means that to update a child widget you get a reference to it and use it like any other Python object. Here's an example of an action that updates a child widget:

    def action_set_true(self):\n    self.query_one(Switch).value = 1\n

    If a child needs to update a parent, it should send a message with post_message.

    Here's an example of posting message:

    def on_click(self):\n    self.post_message(MyWidget.Change(active=True))\n

    Note that attributes down and messages up means that you can't modify widgets on the same level directly. If you want to modify a sibling, you will need to send a message to the parent, and the parent would make the changes.

    The following diagram illustrates this concept:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbU/byFx1MDAxNv7eX4HYL3uljTtvZ14qXV1cdTAwMTHabmm7QFx1MDAwYrS3vVpVJjHEJYmzjlx1MDAwM6Wr/vd9xkDsxHFcYmkoUF1LpYk9XHUwMDFlXHUwMDFmz5znec45M/n70draenY+iNafrK1HX1phN26n4dn6b/78aZRcdTAwMGXjpI9LXCL/PkxGaStv2cmywfDJ48e9MD2JskE3bEXBaTxcdTAwMWOF3WE2asdJ0Ep6j+Ms6lxy/+P/boe96N+DpNfO0qB4SCNqx1mSXjwr6ka9qJ9cctH7//B9be3v/G/JujRqZWH/uFx1MDAxYuU35Jdcblx1MDAwM6W2bPr0dtLPreWGXHUwMDE5LomsXHUwMDFht4iHT/HALGrj8lx1MDAxMYyOiiv+1Hp7I1x1MDAxOVxmXlx1MDAxZf4uP71qXHUwMDFldk5cdTAwMWJCfNz5XFw89yjudvey8+7FWIStzigtWTXM0uQkelx1MDAxZrezztXQlc6P71x1MDAxYiZcdTAwMTiG4q40XHUwMDE5XHUwMDFkd/rR0I9cdTAwMDBcdTAwMWafTVx1MDAwNmErzs79OVa84MUwPFkrznzBN6VYwPFHSSdcdTAwMWOXjunxZd9cdTAwMDHOXHUwMDA2TFx1MDAxMmdCa+Gc4VOGbSZdTFx1MDAwNlxm+4WOZLulXG7TXHUwMDBlw9bJMezrt8dtsjTsXHUwMDBmXHUwMDA3YYopK9qdXb4yJ1x1MDAxYmjmpOKKlDFE41x1MDAxNp0oPu5kaGJN4Egz6yRcdO6kdoUxUT4rVlx1MDAxYiVcdTAwMWOZYvi8XHUwMDA1g6127iF/loet375cdTAwMWO2/qjbLYz2XHUwMDE3nk17VdmzJrwri75cdTAwMTRvUvKEdCdcdTAwMWWdb1Bz1Njdf7v59Vlj820nXlx1MDAxZrf79tvsbi9ubkafs99Pn37gpn9+4I6V2Op3pp5y9fwwTZOzRftcdTAwMWQ4+vzcbDe/dj+yNOp8lVx1MDAwM2lcdTAwMGZW0O+zt9m2ftE8dPZsZ1s8c3uvKVmFve+fvZJvXHUwMDA3XHUwMDFmtYuy5qe9/us/WKPXWazfy0/FhI9cdTAwMDbt8Fx1MDAwMrhcXFx1MDAxYsuEs0qRLlxcvVx1MDAxYvdPpn2hm7ROXG6sPypcdTAwMTlcXGGZXHQ/KFx1MDAxM4w1NH16TDDWOSGVYHZhgpntVktcdTAwMTHMNI5vkWBcZlx1MDAwYpSyhitNmoQuXtffr7hccpzj3Fx0xom70mCsml9cXNH1mFCo8IBLXHUwMDA2IW1cdTAwMTiXJTO+l0BW6YPFTCf9bC/+6lx1MDAwN1vIwFx1MDAxOKOc0lJcdTAwMTitmJto9Dzsxd3zibnLfVx1MDAxNYO1m4/Tr/8qj+gwglx1MDAxMb5XZSfab3TjY+/P6y3cXHUwMDEypVx1MDAxM66exZDmcYPDJMuSXtGgXHUwMDA1I0L0mW4tXCKRSVx1MDAxYVx1MDAxZsf9sLs/beNcXPTN1XjFLK+FoDRcbjOxOFx1MDAwMF80P785y87Tk8M/PvRenGyop6/l2d1cdTAwMDLQXFyHP2FUoJ0w0mjOlZN8UuDJWK//Vlx1MDAwYoDTcuFqXHUwMDAxyPLjblx1MDAwNVx1MDAxZfZJRCpOr1xmn8tcdPxr2j7/LyUv9ejAfvncO2Bb7tPxzyrwbz5svd9I3iE0fNVP4iMxXHUwMDE4ZsasSIitI0xp4di3JMSIXG51XHUwMDFkXHUwMDBiQJtcdTAwMDRBf1x1MDAxNmaB2ZN/v1lAQmeVM2BDKa21JZnwt2tGXHUwMDAxlFx1MDAxY3PBXHUwMDE4Mabp1kiglEDMUWEr4Gyal7Kz25NheKDmdyfDm5242/7BKnyNjE2r8JWJ3yHC4PNaXHUwMDExhi44y1x1MDAxMFx1MDAwMS5cZkDqf+2evmxcdTAwMWOIvZPPO/uHZ+HWwba551x1MDAwMNRcdTAwMDJcdTAwMTCDe8DhhFx1MDAxNFx1MDAxNVx1MDAxMTZwXHUwMDFmidRVO01GqlvD30pEWCHFdkqy1cFzOVx1MDAxNY7C072NdP9Mmv2/tt8kfGh3mns/QNV+wnSYXHUwMDE0XHUwMDE38tZVXHUwMDE42VUtXHIwqaWU7lx1MDAwNvW22dN/z2lA+3zXh+NETDNpJnhAM1x1MDAxNVx1MDAxOMRDllx1MDAxOKJcdTAwMTI9p9r2Y3TYOKRcdTAwMDZCKLUyoK/SXHUwMDA3XHUwMDFmvFx1MDAwZV+jY0vp8Fx1MDAwNfZngE9oUa/BllxmyF6oxVPh+UnMfS12W1x1MDAxNzhcco+2UivLidRcdTAwMDT8oGtcdTAwMTBH4sxoq1x1MDAxOLPThlx1MDAxNfDjVqvw8DuKUSpAXHUwMDFjYJhcdTAwMDLtcTzRVNFoRIDQXHUwMDE00TozTlx1MDAxMKeKXGZjvlx1MDAxMFCQpZtUu1x1MDAwYmG58lx1MDAxOHF55ttyoF26hjXMwjRrxv123D+eNOxyVWeRilGO69bIW9lggVx1MDAxMFx1MDAxY1x1MDAxY8KJWbJWXCKyLzU7XHUwMDBlXHUwMDA3efhcdTAwMTNoLlx1MDAxZIfN1nKjuKy8fdRvX2/V/FxietIqXHUwMDEwkZPge8hcdTAwMWLhqFx1MDAxYeVcdTAwMDKtlOKwyFx1MDAxMUlesahcdTAwMWJcdTAwMGWzzaTXizNcZv1uXHUwMDEy97PpIc7HcsMjv1x1MDAxM4Xt6at4o/K1aYpcdTAwMTj4XHUwMDFlJ6Ow4tNagaD8y/jzn7/NbN2QNmCOw3G5VVx1MDAwZTGk1OX7XHUwMDE1hoOUVYj3cZVLc21/tUjxR1x1MDAwNSNFd4/K/9+YLJWoXHJUyFxi6Vx1MDAwNWbxOGV+pLlcdTAwMWGqbCfeOVx1MDAxNudKupYqSVx1MDAwNcz4ckH+skpPZiyai0AppoEgXHUwMDAxPiUzZdgqM1x1MDAxNlxuYIxcdTAwMTKet5mRZlx1MDAwNlfCXHJcZlx1MDAxN4asYX6lUlbL+iSVsfpuqXLp+GZBqrxcdClcdJJcZrNmpDbMKWZLsLpkJcFcdTAwMDLIoCGlreRcdTAwMDaR6XJMOT/GKVx1MDAxOcWCXHUwMDFj4dxIJpnmXG6onknfXHUwMDFhyavQxLWDUfSgybLetf1RcepcdTAwMWKSW1x1MDAxZbzO4DZuarnNK1x1MDAxMIaWXHUwMDE3VYHruK3RODxcdTAwMTl0e413nz703n462uc7NnyzXHUwMDFjt01cdTAwMTc9bi9cZoRPXHUwMDA3ym92IORcYkK5qWJcZiSHXHSGLFxmLlxi4qintpZcdTAwMTOhXGKXpzZJXHUwMDAxQWl8XHUwMDFjKiGYppSAjalccnpqXHUwMDEwqXCutJLWQVx1MDAwMitxoECw6inmzrhccjKAaEtcdTAwMTdcdTAwMTO4PLdNY7HmyopRPnFtxfFQ7Vx1MDAxY/ujOrsrXHUwMDAyuWD1q57EuGWalVxue9eh/JXZ33p3tN//a3evXHUwMDExd3Y/7vY2yzi+pyjnXGahKHNcdTAwMWF6XCJcdTAwMWSjyVJcdTAwMGI5XHUwMDFl+HqTXHUwMDE2mlx05Ma3XHUwMDA3cul11GJCuJRCilx1MDAxOeFcdTAwMGLoXGJcdTAwMTk4poWk1UaUmlxcVVxckUEohqt3inFokSpM+z/Gr47aXHUwMDE59kdlbm9cYvD6XHUwMDE0xcnps+NcdTAwMTRFwFuM0IunKPOXju9pNUdqXHUwMDFiKM9kklx1MDAxY1x1MDAxM0pNXHUwMDE2c0grjDyzSmryy1x1MDAxOfVrKpE25nsyXHUwMDE0K1x1MDAwMzyFM1x1MDAwYiVcdTAwMTZOmFx1MDAxOWsqXHUwMDA2TSxcdTAwMTfqosTNbaWY44zz+UkxXHUwMDEwP2Ep5yb5XHT32039tjHpk49S4lbkXHUwMDAy0pfymN9GZ1x1MDAwNMPs28rbL5Sg3KjAhEArz4c1hEXzqk0skPA0MLVcdTAwMGaJyIxcdTAwMGJcdTAwMTJcdTAwMGYzP/HFdMF93VxuaVwiXHUwMDFj1JXvbihcdTAwMTko5vNIvKt23OrruqvFSd5dXHUwMDA1XCKrXCJKcHLtJkxcdTAwMTCHVCRcdTAwMTYve89fhbunREkqr0JcdTAwMTJcdTAwMTG3xu+PniBK5D+B4yRcdTAwMTTmXHUwMDA1woHxqGXK7y17I71Hwmu19lm8sULPqnv7QoBcdTAwMTVcdTAwMDC+c1x1MDAwNKtLw3RcdTAwMTlcZjGEyoIxW9j5XHUwMDEzXHUwMDE2c1x1MDAxNqYlsJIzoCUlXHL+Icx1pVxm44qXMMPgLtKCIVx1MDAxM1x1MDAxMXrJqvdccmo5/udcdTAwMDJKYKqM407MsIhcdTAwMDXgUWNcdTAwMTCngDyMIFMx6SExpVx1MDAxNFx1MDAwMahcdTAwMTDqIJj0xepcdGqTKlBcdTAwMDZcdTAwMTFcYsf0cKmEvK63epj4o1x1MDAwMpBVXHUwMDExJWZrzs9hpDQgS7Z4TDl/I8Q9pUptKPDcXCKl31x1MDAxZlOiwjykJFx1MDAxYvhanZXI11x1MDAxOVx1MDAxOODWQkpHgfNLQlpcdTAwMTE8h5XixTFPalx1MDAwM6+CjVx1MDAxMpE+VNNUat7cec1G9Fvc/Fx1MDAxM/LkTerLXG40o1x1MDAwNVx1MDAwNtRxSZzPiCldQIjsuEDCRuAtW2Wl1caULIBNiLBcdTAwMWPYm/zCs6pcdTAwMTbiPVU643dDglx1MDAxMrhGXvOgqbIhXHUwMDFj0mXn02XwpCPi5dtcdTAwMWKKw/MtoCfhv1baa8myUYuV/GpcdTAwMDUmN2TLur1Mtv6HPbBcdTAwMDQxp6XC7a+jyk7nTTM92n16MNJnh9REYrT//PzeUyVcdTAwMDZcdTAwMWUyZIB0+G15xfTqh4O+oCm130RcYlx1MDAwN741ruRyxi97RKVMzpGpSFx1MDAwZeYuLLm1zUxcYscs5Hupgnh5M1x1MDAxM5s4O2f3Ulx1MDAwZuaGx9Fw7dfRYPZcdTAwMWUmXrOHqVx1MDAxYlx1MDAxZE369+RcdTAwMGWmLFx1MDAxOdRtX5p4mem9SpNcdTAwMDYthTBobFx1MDAxZMSQYlvos1i8gp18jJ/2qdGmdCS2XHUwMDA2zVx1MDAxZGaH2+9cdTAwMWVcdTAwMDLCXHUwMDA09EprST66nMrbiFx1MDAwN2SsX1xi9Vx1MDAwYqeO32LeZtRCXHUwMDEw035DkrP6x0DMr1UuVY9eXG5iYZal8eEo8z7dTs7691x1MDAwMmZVoy6g9uhSLtfDwWAvw7iNg1x1MDAxNcxI3L58+aLr9dM4OmtWveKXo/zwvebw9UCJ8jjx26Nv/1x1MDAwMC2LavoifQ== Parent()Child()Child()messages (up)attributes (down)"},{"location":"guide/widgets/#messages-up","title":"Messages up","text":"

    Let's extend the ByteEditor so that clicking any of the 8 BitSwitch widgets updates the decimal value. To do this we will add a custom message to BitSwitch that we catch in the ByteEditor.

    byte02.pyOutput byte02.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    class BitChanged(Message):\n        \"\"\"Sent when the 'bit' changes.\"\"\"\n\n        def __init__(self, bit: int, value: bool) -> None:\n            super().__init__()\n            self.bit = bit\n            self.value = value\n\n    value = reactive(0)  # (1)!\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n    def on_switch_changed(self, event: Switch.Changed) -> None:  # (2)!\n        \"\"\"When the switch changes, notify the parent via a message.\"\"\"\n        event.stop()  # (3)!\n        self.value = event.value  # (4)!\n        self.post_message(self.BitChanged(self.bit, event.value))\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n    def on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n        \"\"\"When a switch changes, update the value.\"\"\"\n        value = 0\n        for switch in self.query(BitSwitch):\n            value |= switch.value << switch.bit\n        self.query_one(Input).value = str(value)\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n
    1. This will store the value of the \"bit\".
    2. This is sent by the builtin Switch widgets, when it changes state.
    3. Stop the event, because we don't want it to go to the parent.
    4. Store the new value of the \"bit\".

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a32\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2503\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2503 \u2503\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u2503 \u2503\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    • The BitSwitch widget now has an on_switch_changed method which will handle a Switch.Changed message, sent when the user clicks a switch. We use this to store the new value of the bit, and sent a new custom message, BitSwitch.BitChanged.
    • The ByteEditor widget handles the BitSwitch.Changed message by calculating the decimal value and setting it on the input.

    The following is a (simplified) DOM diagram to show how the new message is processed:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPiSNL+7l9BeL/MXHUwMDFiMdbUlXVMxMaGb7vb99l4e8MhQLZlZKBBPjfmv29cdTAwMTZ2I4FcdTAwMGVcdTAwMDTGgHteOtrYkiilqvLJfDKrsvjvQqm0XHUwMDE4Pre8xT9Li95T1Vxy/FrbfVxc/N1cdTAwMWV/8NpcdTAwMWS/2cBTrPt3p3nfrnavvFx0w1bnzz/+uHPbdS9sXHUwMDA1btVzXHUwMDFl/M69XHUwMDFidML7mt90qs27P/zQu+v8y/7cc++8f7aad7Ww7UQ3WfJqfthsv97LXHUwMDBivDuvXHUwMDExdrD1f+PfpdJ/uz9j0rW9aug2rlx1MDAwM6/7ge6pmIBcdTAwMWH44OG9ZqMrrSGEK0JcdTAwMDTtXeB31vB+oVfDs1cos1x1MDAxN52pdVx1MDAxZs9cdTAwMTfl/ZPjQ7ax/0Rf/MOL5tI2j2575Vx1MDAwN8Fx+Fx1MDAxY7x2hVu9uW/HhOqE7WbdO/dr4c3PnotcdTAwMWTvfa7TxF6IPtVu3l/fNLyO7YBI0GbLrfrhsz1GSO/oay/8WYqOPOFfXHUwMDAwwpHAiFRGXHUwMDFhQ0x05+7nJXWEUExSpoXgQOSAYKvNXHUwMDAwx1x1MDAwMlx1MDAwNftcdTAwMDdcXPFaVUSiVdxq/Vx1MDAxYeVr1HrXhG230Wm5bVx1MDAxY7House3R6Y8uvWN51/fhHhQRlx1MDAwZtXxulx1MDAxZFx1MDAwZlx1MDAxYSRKZFTvhL1Ja7vW1YH/xHumUXvrmcZ9XHUwMDEwRHLZXHUwMDEz64N6XHUwMDEz150+/Vx0vadI2NhgP1x1MDAxZl3723opuPpa11+vvJ1rslx1MDAxNJ4v9q776/f0Zl8/zF42audnXHUwMDE3x0vN3X3plVx1MDAxZla/3Kq1/rv8vL/bbjdcdTAwMWZj7b79XHUwMDE2Peh9q+a+6iSVSlx1MDAxM6FcdDdMR4NcdTAwMWP4jfpgXHUwMDFmXHUwMDA0zWo9UuOFmMBcdPz0PX9cdTAwMWM6hoos6FDNmEFRiC6MnfTuXHUwMDFjXHUwMDBiO3R62FE0XHUwMDBmO5o51ExcdTAwMDU7RiehQ80gdKhcdTAwMDZcdTAwMDZcXFGYXHUwMDE4dLK0kDJKiVx1MDAwMjKKXHUwMDE2RmPdbITH/ktXkWTf0Vxy985cdTAwMGae+4arq57YPSvPobfedVxmv/1fvFx1MDAxZjtcdTAwMWXeudtcdTAwMTTv+8xy4F9bNV6s4rN47T5cclx1MDAwZn30Nb1cdTAwMGLu/Fot7j2qKIiLbba3i1x1MDAxOP1m27/2XHUwMDFibnCSJmcu8HJcdTAwMWRcdTAwMTdHdGWhj1FcdTAwMDaGoz6qwujTle3d1fKjvOzcbLeCSiB2XaPn3nNJ7lx1MDAxMK0oVcSAVMz0oY9p6aAx1Fx1MDAxYzVeXHUwMDAyZVx1MDAwND5cZn2o60U8XHUwMDE32lx1MDAwMkEkxaGZsetqy/UtVl7+tvTilqv1tc7S4flyY1xuriu3XbF3V79cclx1MDAxZaTim+et2y/N8mXdrU+g3VBcdTAwMDSbT4dXXHUwMDBmXHUwMDE1oYOV2vPm84/bJz1cdFerNChD0PhHivVBrpZrk8lSKTIhTlx1MDAwNep3YbCnXHUwMDBm/ydwtdlg50Q6MFx1MDAxZLBcdTAwMWKRxHqKq1XGXGJ0+zGv81x1MDAwYvlaPzx+9MPqzW9qur52iJtK+NqYnKV8Z/tcbv007IEhmdijVCilmCpcdTAwMWVcIuZbzznFnqTSQTZhXHKe6uKv39Ey6ShcdTAwMTDCWkJqiFx1MDAwMJ2JPdJ9jY894qBcdTAwMDWQXHUwMDFhKbVcdTAwMDEgRpuUiFx1MDAxMYgjXHUwMDE41VQrJVx1MDAwNIYgbFx1MDAxMJqGI2Y4k6NcdTAwMDSQkV/5qTHs7chfo1x1MDAwM/ZniEbHXHUwMDAxbCd02+GK36j5jet+wd5SIUVYqX1mt4XXXHSHXHUwMDEzjFx1MDAwN1xml0pII6L+7Fx1MDAxYYHqfafb6Tiw1t1cdJAouFx1MDAwNtCJZ/dcdTAwMWG14TLlo7cnk8ZBRlx1MDAwMov/0LlyXHUwMDAxXCJdJlxmuqjSXG7sf1x1MDAxY26iXHUwMDEyQlx1MDAwNW4nXFxt3t35Ifb9QdNvhIN93O3MZVx1MDAwYv1cdTAwMWLPrVxynsWHip9cdTAwMWK0XHUwMDExLdtiP1x1MDAwYot+K0VcdTAwMTDq/tH7/T+/p1+dqdn2ldDpqLmF+PtYoVx1MDAwNDbHXHUwMDA2XHUwMDBm9yxcdTAwMWNcdTAwMTJcdTAwMGJcdTAwMWNwXHUwMDE5c7fDLFxc5UTedbbWn/ZcdTAwMGVEsFX/8m19tbV1PFtcdTAwMGKnhpJcdTAwMGJcdTAwMDKOUlx1MDAwNrhcdTAwMDTgQotcdTAwMDFcdTAwMGKHps1yXHUwMDBmSz9cYmp/pJFcdTAwMTM3cKmRXHUwMDA0RMfeTFx1MDAxOGPaUFxudNY5sCevfHZ5e7N50ZKrrfrB8u7LVc2fXHUwMDAy4Z9cdTAwMTdijujIzoFcdTAwMTmUwlx1MDAxMCOL58DSu3POoYPYYFx1MDAxMXRcdTAwMDaAw2FKwIk501x1MDAxY1ZutFx1MDAxMFIjaZtcdTAwMWEpXHUwMDFmy8ePRcp33IpcdTAwMTf89n1RfV9EsjtNVj7E4lx1MDAwZrLyfkHf4bdoXHUwMDBlM1x1MDAwNyqBXGJcdTAwMTGLm4eBr3a6t7dzclY7P0TQ3Vx1MDAxY79UV59cdTAwMGXUnINPcqTehHGMM20kovvzz1x1MDAwMmk7kUxQJJyKXGIjs4PiKfktqlx1MDAwNMbnJJ7OmI3jkpvkaUM9fttvXHUwMDFmeU+rXHUwMDE3661V9+jub5RRXHUwMDEyLCejhOacauQ8xTNK6d0559hcdTAwMDHlsGzscDot7EiThE7SdXHKXGaGXHUwMDA1ZnpzN9NzXW9Jmim7rSFcdTAwMDZ/0G1FQuZcIi4zkcQgliZcdTAwMWGAnNboy/goUVY+eZ7XPFx1MDAxMoZZVDFUY2lcdTAwMTBYnPZBjlx1MDAwMzhcdTAwMWPhyKnmiEnGP1xmcYDYNpJcdTAwMTjOXGbjUsTuXHUwMDE0+S7pUINcdTAwMDNiU+tKMMVcdTAwMDfxSFx1MDAxObOeN25cdTAwMTimm0ZcdTAwMWHLWcS6tFBcdTAwMWGpcMpcdTAwMDZcdTAwMDNSMFJcdTAwMDB6d8K0jl3xM2WzZONcdTAwMDTCKJGK2excdTAwMWJRSiZcdTAwMWW+UFx1MDAxZSmfb8aEXHUwMDAywOFlaDhcdTAwMTUnPDJH/UJcdE1cdTAwMTWnwIBcdTAwMThhM2BcdJk+U1x1MDAxYWkpU7XtK6HUUXNcdTAwMGLx99FtXHUwMDFidlxcpm0jXGJ5SiFS02G2LZ9fzatt48xBViVcdTAwMTRcdTAwMWE3XG5aRqHuq20zXHUwMDBl2jeBJyQ3mis1INhkjVx1MDAxYva3wpDbKIRaynSVQLyitlM0XHUwMDFlXHUwMDA2JY5f8zPBRJTQXHUwMDA0Ufr/1s2aeodcdTAwMTMp0WppJVx1MDAxNGozxC6JMtJcdTAwMWOQMnNjKHInKWQyXHUwMDFmXci45bOSmHFcdTAwMTNGXHUwMDAxt/8p1TLVuKFInFx1MDAxYqIpJ8SumDNJe/uZbFumYttXUqVHtG25qVx1MDAwNqpoJndD5ibRmlIovtqGtOvbX7dcdTAwMGWDXHUwMDEzctmRrdN6eHrS+jL3XHUwMDA2zlx1MDAxOIcx27WMaWl0xO67k4CGOVRqVDc8XHUwMDBm6G6z7VtFMVxyufbt7Vx1MDAxYSGS1o2ylGhcdFx1MDAxMvZcdTAwMGJVRFxuytHQvst+0eH2q/eZkfJcdTAwMGbaf1bNaq3R2Vx1MDAxNPvqWMOB6bxcdTAwMTROnMPJ9cph58vt3XVjjSw/XHR2tr4rP1f+gVwiWcxcdTAwMDJcdTAwMTRGQtTOXHUwMDAwysJ4Su/NOceTXCJcIlx1MDAxN096cnjKT92RlPCHJ/JcdTAwMGZMXCKbVFx1MDAwMqa3noWNoITRWMfyXHUwMDBm1EHDzImWTKBKkdhUTV8+oj+9sNhcdTAwMGL1ndVcdTAwMWJcdTAwMWM5r/bb94Y9++BcdTAwMDb33j9P2vfe90b6qlx1MDAxNy77WurlIVx1MDAwMu+qXHUwMDFmXHUwMDA0I6UphjiL9DRFruzjcXyusyk+XGJcdTAwMGV8hEKJfFx1MDAwYjZcdTAwMTnA1tzOjTdRxGqGPk5TXHUwMDEwXHUwMDEyQCPp6l+CJoh0XHUwMDE05UA018hR9MchXHUwMDE2hIO3oFx1MDAxOHNR5OiKcEhcdTAwMDJcdTAwMThcdTAwMTRyViosYUJahOBcdTAwMWTEM0ib1Fx1MDAwND5cbp4nTvC54eojXHR++NU/OHKXbzfPt063j2twpra93TiZjqgyM2huKTfUXHUwMDE4ZVikyz3GXHJcdTAwMGXaarxCXHUwMDAypZxRNuZcIph8OPeLpDhcdTAwMTeW7uJQXHUwMDAz4TIhXHUwMDEyd5jgikpk91xmLTP/7LmLTL1+PT2o0pNk+JzFqOLgenpAXHUwMDFlaVx1MDAwN7+wfSt7QeXg7GqLPD7Cycry/ZHoXFzuz/1yeux/ooFcYlx1MDAwNmBcZiH9XHUwMDE5XGbQxulcdTAwMDaTimMsqSE7gfH+1fSRsYrsWYKQaHTsXFxyMevF9GflzfJcdTAwMTKtrHjV1epcdTAwMGZf3vDypbgqSuXp4cF6cNsgq/7h3kZwtXfyrHaCidSBWVx1MDAxNtWN/j+aynNmMpFDQWpcdTAwMDPoXHUwMDE0WWHopHfnnHP510qUXGI6/VxcXlx1MDAxMuOg3ZpcdTAwMDJ0XG6WgVx1MDAxMYw7qITY/MtHcvlRtTCVy89/XHUwMDFk2Fx1MDAxMJv/QXVggtHMRTCAPotJw4tcdTAwMDfSO/ri6kbpx1v/oHy+fPz88Lgmzubeb0nmXHUwMDEwZsBcdTAwMTBKpCZU94FcdTAwMGa7XHUwMDAw/Vx1MDAxNmBcdTAwMTQtKFKKWFx1MDAxMDOrKjAlJGKPTbAyZDzH1VDrbnAsXq7Kl1qrs8ZdsIy+5+NcdTAwMWRXbruHpzWobZRltVx1MDAxNXYuqD5mlaO9/Vx0tNtoP/JNODva6ZgnODhZXHRX7vjW53K0gsVcdTAwMTZ/JCpRrOtcdTAwMTkpaZY+/PPuaFx1MDAxNcnDumCOnlxu1otcdTAwMTWBcTvNSmBKa3am7GdnVVx1MDAwMzbER31EXHKYkCxz+sdmgjhBT1tcdTAwMTh4+aZzToEnKXGEsrVCXGZcdTAwMTksVf25L1x0zFx1MDAwMZDKXHUwMDA2yoxzPSjX5Ga3hWNX7DFcdTAwMDWKo1x0gFx1MDAxNFx1MDAxOErqoFx1MDAxOVSaaWWUMDpJf1FAjpxcdTAwMWNmVlx1MDAwMNZFq2CxXGJ78qmvfDpaXHUwMDFhLO/SXHUwMDFjiNLG1lGp+Oqdn2ViXHUwMDE4Llx1MDAxOIGBg1x1MDAwMmCCQ+LZXHUwMDBi5b7yoVvKLO/CuybzcdpB6ElDqVx1MDAxNFx1MDAwNohmn3tyO0uv7Suh0VFjXHUwMDBi8ffxXHUwMDAyXGIhs6e2hUShsG8jRVx1MDAxZGbc7k9b6ujBe3jY2Hrcba1cdLPjXHUwMDFkzdi4XHUwMDE1qP7iaNuwbzU3nLBcdTAwMDFSgdbOXHUwMDAxQrStuELkfmB1a9FF9MxWwaNGzDqA8I8qa1crtdtccvdCfDup1Fx1MDAwM1J396dA9OeGkEM2dFBPXHUwMDEwWECL04L03pxz5FDlmGzkSDkl5Fx1MDAxNKv+UoxcdTAwMDDGSXJyXHUwMDE54/mh47Or/lx1MDAxYWLxP6z6XHUwMDBiVOZcblx1MDAxMqQo1Fx1MDAxOGGKXHUwMDA3w99u7veO/fLVKWeN3VbraYtvm4c5XHUwMDA3XHUwMDFm+mZHcIyDpTJMx6ffX91cdTAwMTaSKGq3YEPk0fhsznTcVjLvhUxcdTAwMDOMgZnvfnS6slG5fdg/uEJd9Y/LnFc3Ksd/pzySjFx1MDAxOH5yMaOmwG1qpTB00rtzzqFcdTAwMDPaMdnQkeDAVKBTrPSLMqa7ZWi/4nzNbGq/htj7Sdd+cWmyIVx1MDAwN5JQu1a1eJCVz53nNYNEmMNcdTAwMTlGuJJcdTAwMTDDgffvXHUwMDE0oFxmdaRcdTAwMTZcdTAwMTjjaoVRp1x1MDAxOZw+mmBcdTAwMDbJOCCMUZJwgy5Jpm0hhFx1MDAwMbe0Y4ZgQHljXHUwMDBiP3r7b0hQo/ixT5c+KpyqsWVd0ih0XHUwMDFjSPPRoEZ7kpSyqsN44tFcdTAwMGJlj/KJZim7qFx1MDAwYjiFXHUwMDE0ofqqw0RCps+UPVrKVGr7Sqhz1NxC/H10s2ZU5s5BjFFcdTAwMWPzUTh4PrWaV6vGucPRbFx1MDAxMSYpKvfAKm5lwDFCITaowivIx+XFke1TxolmVlx1MDAxOMVT4mFcdTAwMDFcdTAwMGW39Y1GIErBJEsmUH2UlCNFx7+uWXst51x1MDAxMnainiB2XGKw5PJLaic9bEZcXOFVaEjUmCVf+WSklF3QlVx1MDAxNEk7SGmFIYBcdTAwMDZQXHUwMDEzwz73itAspbavQXVcdTAwMWXRpuV/K1x1MDAwMIPsJW2ca6kkXHUwMDFiIUI65N5cdTAwMGVpiy1cdTAwMTnenZZbT5VvlVx1MDAxM3ExW8Mmhld7Scfmy4TQXHUwMDAwXHUwMDE4gPTvjqFcdTAwMTh3hMUqdoPdXHUwMDA0M5utuZJ6opZr1/7hSWE3i0+r9lx1MDAxMilcdTAwMGLaIEHHMEzjwtD3bun4YdVe9y7dXGb8h/37L/5hsKb3g43640rRjFx1MDAwM/9xcv+1/nxSP/56015fuffPrlduP1fGgdHsxdVUgFGCY1RQXHUwMDE4T+ndOd94wuA9XHUwMDBmT5xOXHUwMDBlT/nJurTNLpLVXpTYRJ31NCMgasYph7HLvXpLROak4muIv8hcXOEyftFXvjfkMnPdXHUwMDE59jVcdTAwMTOKj7DHobtz0Vxcalxcr9HTXHUwMDAzXb/wNm+bZ8/e3LN8Y3fO0EaBVlQgw+pDL1x1MDAxOEQvVcyWR1x1MDAxYVx1MDAxZV/sPlx1MDAxYm8ojWGESP2+vVx1MDAxYj7MXHUwMDFidiqHoVe+8E7MQV2dtFdum5Xdtb+TN2RcInu/XfRcdTAwMTNcdTAwMTKdoSjuXHLTu3POXHUwMDAxZd1hXHUwMDFloMzkXHUwMDAwNVx0d8hcYjVcdTAwMTQ0+URcdTAwMGI5f1x1MDAxOW84xGF8gDfMzHdRqTPT+Fx1MDAwNlx1MDAxOLWhd/F8V74pm9dcImgllUOUNFx1MDAxYdWKoaPpX1xiqqh0tOXz1NhccnNIzjZH7922wFx1MDAxMVozSbjmWktqTErRoOSv+11cdJTHriZMbuJmP6pcdTAwMTSwWVVBTyXplc8nS32Jc8LRJNtcdEfVXVwiK2M5l7dcdTAwMTSTdIBLpYjm3e9bgTHXgubDutS3XHUwMDE2XHUwMDE0PVx1MDAwMZPC1lxc21R+MuslXHUwMDFjprlB3VVgR1MlJ1x1MDAxOD5T1mspW7W7p1x1MDAxM1pcdTAwMWQ1uFx1MDAxMH9cdTAwMWZzp6PY5PlgSlx1MDAxZs2fVYziu1Tm19/PhJvAcFx1MDAwM8fsvrCCXCI/wchnoJZTULtcdTAwMDeE5Izh8NiKz5x9Yaex0Vx1MDAxMVxiYlBcdTAwMGJgXrn+16PDcufMO79i4erezsHDt6WD2nzsczTq3lx1MDAxM2NxfVtlm8n1XHUwMDE5t7KoXHUwMDExguf07px3PKlcXDzpXHTiaVx1MDAxMlx1MDAxYlx1MDAxZFEgqFx1MDAxZDJe8vPRO1x1MDAxZI3l/H+9nY6GeItJ73SUidlcdTAwMWNcdTAwMGaImJVSk1x1MDAxMVxuXCI6wU1l47D98uOyeaafVn/slDfXZuxcdTAwMDJcdTAwMGKUWVxu4oDgXHUwMDE4fVx1MDAxYrtiU/av61x1MDAxNkY51FZZaqT5hKpY0elkfSCPfcFNXHUwMDBmsbEvOowqqi3t0yNtVjo+Ylx1MDAwMVx1MDAxOYJ6L2JcdTAwMGKvj1t2Sq+KXuogXHTtlPq1vnSHT+Nee6kojX0/8ygoXHKbrSyI9j3lIFx1MDAxZVx1MDAwYko6XHUwMDE2JmVOkZKynnyETVx1MDAwZfjT5Y/VJ7qp1lx1MDAxZlr8eV08Xla/VudcdTAwMWWRhDukXHUwMDFidFx1MDAxM40/419waVx1MDAxYjDMfoFcdTAwMTVgPIbRpCTZlVx1MDAxNkUy0NmAjH+bXrSwJPn1VGg60EqS6UxcdTAwMWVJSsW7PWjxumen1Esxldpep9W0ul557io9hrylRFx1MDAwMup7I2yW/LBTXHUwMDFhJCZxhzpdqL7/IV5RvPBGsVx1MDAxN91W6zjEcelFXHUwMDE3OOJ+7a1zI1FcdTAwMTZcdTAwMWZ873ElReWuui/batcyWFx1MDAxMHrdUOWvhb/+XHUwMDA3KtdcdTAwMDdDIn0= ByteEditor()BitSwitch(7)Label(\"7\") Switch() Switch.Changed( value=True)ByteEditor()BitSwitch(7)Label(\"7\") Switch() BitSwitch.Changed( value=True)BitSwitch.Changed( value=True)Switch.Changed( value=True)A. Switch sends Switch.Changed messageB. BitSwitch responds by sending BitSwitch.Changedto its parent"},{"location":"guide/widgets/#attributes-down","title":"Attributes down","text":"

    We also want the switches to update if the user edits the decimal value.

    Since the switches are children of ByteEditor we can update them by setting their attributes directly. This is an example of \"attributes down\".

    byte03.pyOutput byte03.py
    from __future__ import annotations\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.geometry import clamp\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\n\n\nclass BitSwitch(Widget):\n    \"\"\"A Switch with a numeric label above it.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\n\n    class BitChanged(Message):\n        \"\"\"Sent when the 'bit' changes.\"\"\"\n\n        def __init__(self, bit: int, value: bool) -> None:\n            super().__init__()\n            self.bit = bit\n            self.value = value\n\n    value = reactive(0)\n\n    def __init__(self, bit: int) -> None:\n        self.bit = bit\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(str(self.bit))\n        yield Switch()\n\n    def watch_value(self, value: bool) -> None:  # (1)!\n        \"\"\"When the value changes we want to set the switch accordingly.\"\"\"\n        self.query_one(Switch).value = value\n\n    def on_switch_changed(self, event: Switch.Changed) -> None:\n        \"\"\"When the switch changes, notify the parent via a message.\"\"\"\n        event.stop()\n        self.value = event.value\n        self.post_message(self.BitChanged(self.bit, event.value))\n\n\nclass ByteInput(Widget):\n    \"\"\"A compound widget with 8 switches.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for bit in reversed(range(8)):\n            yield BitSwitch(bit)\n\n\nclass ByteEditor(Widget):\n    DEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\n\n    value = reactive(0)\n\n    def validate_value(self, value: int) -> int:  # (2)!\n        \"\"\"Ensure value is between 0 and 255.\"\"\"\n        return clamp(value, 0, 255)\n\n    def compose(self) -> ComposeResult:\n        with Container(classes=\"top\"):\n            yield Input(placeholder=\"byte\")\n        with Container():\n            yield ByteInput()\n\n    def on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n        \"\"\"When a switch changes, update the value.\"\"\"\n        value = 0\n        for switch in self.query(BitSwitch):\n            value |= switch.value << switch.bit\n        self.query_one(Input).value = str(value)\n\n    def on_input_changed(self, event: Input.Changed) -> None:  # (3)!\n        \"\"\"When the text changes, set the value of the byte.\"\"\"\n        try:\n            self.value = int(event.value or \"0\")\n        except ValueError:\n            pass\n\n    def watch_value(self, value: int) -> None:  # (4)!\n        \"\"\"When self.value changes, update switches.\"\"\"\n        for switch in self.query(BitSwitch):\n            with switch.prevent(BitSwitch.BitChanged):  # (5)!\n                switch.value = bool(value & (1 << switch.bit))  # (6)!\n\n\nclass ByteInputApp(App):\n    def compose(self) -> ComposeResult:\n        yield ByteEditor()\n\n\nif __name__ == \"__main__\":\n    app = ByteInputApp()\n    app.run()\n
    1. When the BitSwitch's value changed, we want to update the builtin Switch to match.
    2. Ensure the value is in a the range of a byte.
    3. Handle the Input.Changed event when the user modified the value in the input.
    4. When the ByteEditor value changes, update all the switches to match.
    5. Prevent the BitChanged message from being sent.
    6. Because switch is a child, we can set its attributes directly.

    ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a100\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    • When the user edits the input, the Input widget sends a Changed event, which we handle with on_input_changed by setting self.value, which is a reactive value we added to ByteEditor.
    • If the value has changed, Textual will call watch_value which sets the value of each of the eight switches. Because we are working with children of the ByteEditor, we can set attributes directly without going via a message.
    "},{"location":"guide/workers/","title":"Workers","text":"

    In this chapter we will explore the topic of concurrency and how to use Textual's Worker API to make it easier.

    The Worker API was added in version 0.18.0

    "},{"location":"guide/workers/#concurrency","title":"Concurrency","text":"

    There are many interesting uses for Textual which require reading data from an internet service. When an app requests data from the network it is important that it doesn't prevent the user interface from updating. In other words, the requests should be concurrent (happen at the same time) as the UI updates.

    Managing this concurrency is a tricky topic, in any language or framework. Even for experienced developers, there are gotchas which could make your app lock up or behave oddly. Textual's Worker API makes concurrency far less error prone and easier to reason about.

    "},{"location":"guide/workers/#workers_1","title":"Workers","text":"

    Before we go into detail, let's see an example that demonstrates a common pitfall for apps that make network requests.

    The following app uses httpx to get the current weather for any given city, by making a request to wttr.in.

    weather01.pyweather.tcssOutput weather01.py
    import httpx\nfrom rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        await self.update_weather(message.value)\n\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n
    weather.tcss
    Input {\n    dock: top;\n    width: 100%;\n}\n\n#weather-container {\n    width: 100%;\n    height: 1fr;\n    align: center middle;\n    overflow: auto;\n}\n\n#weather {\n    width: auto;\n    height: auto;\n}\n

    WeatherApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aEnter\u00a0a\u00a0City\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    If you were to run this app, you should see weather information update as you type. But you may find that the input is not as responsive as usual, with a noticeable delay between pressing a key and seeing it echoed in screen. This is because we are making a request to the weather API within a message handler, and the app will not be able to process other messages until the request has completed (which may be anything from a few hundred milliseconds to several seconds later).

    To resolve this we can use the run_worker method which runs the update_weather coroutine (async def function) in the background. Here's the code:

    weather02.py
    import httpx\nfrom rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.run_worker(self.update_weather(message.value), exclusive=True)\n\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    This one line change will make typing as responsive as you would expect from any app.

    The run_worker method schedules a new worker to run update_weather, and returns a Worker object. This happens almost immediately, so it won't prevent other messages from being processed. The update_weather function is now running concurrently, and will finish a second or two later.

    Tip

    The Worker object has a few useful methods on it, but you can often ignore it as we did in weather02.py.

    The call to run_worker also sets exclusive=True which solves an additional problem with concurrent network requests: when pulling data from the network, there is no guarantee that you will receive the responses in the same order as the requests. For instance, if you start typing \"Paris\", you may get the response for \"Pari\" after the response for \"Paris\", which could show the wrong weather information. The exclusive flag tells Textual to cancel all previous workers before starting the new one.

    "},{"location":"guide/workers/#work-decorator","title":"Work decorator","text":"

    An alternative to calling run_worker manually is the work decorator, which automatically generates a worker from the decorated method.

    Let's use this decorator in our weather app:

    weather03.py
    import httpx\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True)\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    The addition of @work(exclusive=True) converts the update_weather coroutine into a regular function which when called will create and start a worker. Note that even though update_weather is an async def function, the decorator means that we don't need to use the await keyword when calling it.

    Tip

    The decorator takes the same arguments as run_worker.

    "},{"location":"guide/workers/#worker-return-values","title":"Worker return values","text":"

    When you run a worker, the return value of the function won't be available until the work has completed. You can check the return value of a worker with the worker.result attribute which will initially be None, but will be replaced with the return value of the function when it completes.

    If you need the return value you can call worker.wait which is a coroutine that will wait for the work to complete. But note that if you do this in a message handler it will also prevent the widget from updating until the worker returns. Often a better approach is to handle worker events which will notify your app when a worker completes, and the return value is available without waiting.

    "},{"location":"guide/workers/#cancelling-workers","title":"Cancelling workers","text":"

    You can cancel a worker at any time before it is finished by calling Worker.cancel. This will raise a CancelledError within the coroutine, and should cause it to exit prematurely.

    "},{"location":"guide/workers/#worker-errors","title":"Worker errors","text":"

    The default behavior when a worker encounters an exception is to exit the app and display the traceback in the terminal. You can also create workers which will not immediately exit on exception, by setting exit_on_error=False on the call to run_worker or the @work decorator.

    "},{"location":"guide/workers/#worker-lifetime","title":"Worker lifetime","text":"

    Workers are managed by a single WorkerManager instance, which you can access via app.workers. This is a container-like object which you iterate over to see your active workers.

    Workers are tied to the DOM node (widget, screen, or app) where they are created. This means that if you remove the widget or pop the screen where they are created, then the tasks will be cleaned up automatically. Similarly if you exit the app, any running tasks will be cancelled.

    Worker objects have a state attribute which will contain a WorkerState enumeration that indicates what the worker is doing at any given time. The state attribute will contain one of the following values:

    Value Description PENDING The worker was created, but not yet started. RUNNING The worker is currently running. CANCELLED The worker was cancelled and is no longer running. ERROR The worker raised an exception, and worker.error will contain the exception. SUCCESS The worker completed successful, and worker.result will contain the return value.

    Workers start with a PENDING state, then go to RUNNING. From there, they will go to CANCELLED, ERROR or SUCCESS.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVbaVPbSlx1MDAxNv2eX8EwX2aqQqf3JVVTU2BcdTAwMWN2YzDLkFevUsKWjWLZciSZ7VX++7uSwdq8gnFMxkmxqFvS7dv33HNud/PXh7W19fChZ69/Xlu37+uW6zR86279Y3T91vZcdTAwMDPH60JcdTAwMTONf1x1MDAwZry+X4973oRhL/j86VPH8tt22HOtuo1unaBvuUHYbzhcdTAwMWWqe51PTmh3gv9GXytWx/5Pz+s0Qlx1MDAxZiUv2bBcdTAwMWJO6PmDd9mu3bG7YVx1MDAwME//XHUwMDAzfl9b+yv+mrLOt+uh1W25dnxD3JRcdTAwMTjIhM5frXjd2FiKXHUwMDE10YRiTIY9nGBcdTAwMWLeXHUwMDE32lxyaG6CzXbSXHUwMDEyXVq3znrXZLtd3dpUXHUwMDBm21xyfSrtRilIXtt0XFy3XHUwMDE2PrhcdTAwMDNXWPWbvp8yKlxifa9tXzqN8Fx1MDAwNtpJ7vrwvoZcdTAwMTXcgFx1MDAwMcNm3+u3brp2XHUwMDEwZG7yelbdXHRcdTAwMWbgmsTDi1x1MDAwMy98Xkuu3MNvgjDEXHUwMDE1JpozKolcImbYXHUwMDFh3U6MRsxAo1CYXHUwMDExKnJmlTxcdTAwMTdmXHUwMDAyzPonjj+JXddWvd1cdTAwMDLjuo2kXHUwMDBmXHUwMDExlnXdVCrpdfc8XFylkZGaYENcdTAwMTjWwrBkWm5sp3VcdTAwMTNCXHUwMDFmRZDWXFxJIdNm2PFsXHUwMDEww6lcdTAwMTBaXHUwMDEwOWyJXt7ba8SR8WfaXd3Gk7ueQyVcdFx1MDAxNvZ05WcyjKh/OVx1MDAxZmTpQMtcdTAwMDRbaN+Hw9GlXCKDXHUwMDFlYm653vbRySU+7SmveVx1MDAxZPh368N+Pz+Ofuzg5tbX3bPbg4rg2K0+bjaCutNcdTAwMGV19i3P77d830s/9+mnZPz9XsNcdTAwMWFcdTAwMDQwkVx1MDAxYVx1MDAwYmaEMJypYbvrdNvQ2O27bnLNq7eTmP+QMrhcdTAwMDC2zPjTiYCbcTgjlElCXHKZXHUwMDFkZqOduViYXHUwMDA1XHUwMDFlJJtFokxwxFx1MDAwNadMa0MkTsVpdDulXHUwMDE0acYphVxizob3/ChcdTAwMGJ9q1x1MDAxYvQsXHUwMDFmXHUwMDAytogzo4q4oiyPJsNcZotiY1x1MDAxZTBlXCKmgJpFXHUwMDA2YDLRXjesOY+DZJ25+sXqOO5DZq7iyFx1MDAwNPdUy5XtvcpO2oWBXHIvjUNRZbpvuk4rXG7e9TpcZsP2M3FcdTAwMWQ6QEfDXHUwMDBlXHUwMDFkp9FIXHUwMDEzTFx1MDAxZGyw4Jn+3iy84PlOy+la7lnOxIlIm0hrVLPxcMOGXG6qUi6fhrcq/966XGKCbvussnlMSuLhuH9cdTAwMTj+WryZ6aymXHUwMDExQFx04lx1MDAwYuBmXGalXHUwMDE5vHFsXHUwMDEwIJFTpVx1MDAxOFx1MDAwNN/rWFxyUHttyzdhNcKYoVLNXHUwMDA1xCWy2rfqZVmwb3T/ZrfU8qonrTPaO1pcdTAwMDKrTXxuXHUwMDE4XHUwMDFjXHUwMDE4uSVlu3K811xid75x9ztcclx1MDAxN/Dcsz0lws7Z8Vx1MDAxN1BBzdOr+uFV6bv1vliYXHUwMDE4OjYtQHxqRjgzM6eF0bO/4mlBYkQ4XHUwMDA2b3NNOc2JXc5cdTAwMThcdTAwMTJcdTAwMWFcdTAwMDNgXHUwMDA1l/p1aWEyXHKLYlwiKNKwXHUwMDAyNc5cck9N2u9Dw6fnlcqyaXhcbo/lafjZxJfTME/JrVx1MDAxY95cdTAwMDRcdTAwMTPGXGLK2Mxw278qNbulSvXm6urkXFxaTJ5WW1x1MDAwZr9cdTAwMTZuXHUwMDA0T8NcdTAwMWI1XG5cdTAwMTjQ4NE0XGakh0BcZnNJoHpTUGy/ioeb1jXGYvEsrOHNUFx1MDAxZq8qXHTLi3ur6e5cXDx6m1++063+5Vx1MDAxMS6fLoEsV4XUOFx1MDAxZLuEXHUwMDAzYWUwwXNw2mhnrjjIXHUwMDE4VVBbMjya1GAukMRU86j4pK9cdTAwMDTZRFYjqfQ/gdaI0Vx1MDAwNv5cdTAwMTP2O/JaabNSKlx1MDAxZlx1MDAxZZa3l8psU7ghz2yJkS/nNkHEONhRoYWSxMzObf2gVz06bzfZ0ebBl1p5V5VZzVl12Fx1MDAwMXsjQoFcdTAwMWSE0YRcdTAwMDNcdTAwMDBz3CZcdTAwMDCUUlGJXHUwMDE1YFLr18BcdTAwMGW4TURF+1vUmIxSRoRKXHJ3pehtyznY3WnKq9ZuhXnNvc6J7LR+zEpvj03F5f79t83H8I5cXO79aFx1MDAwNY1cdTAwMGLzzuiN87FcdTAwMWFSSC4wobOv5Ix25qrjTClEgFtkviqLYaZcZpLcQPmqtVSvhNlEdlOkXGKsUeRmKMSHTs3a70Nu5dPT49OlXHUwMDEy21x1MDAxNGLIXHUwMDEz28DAl5Oa5HJcdTAwMWPYiIZMbrjWc+xT6KteKdxix51wrytkTe5vXf7iXHUwMDA1kuloU1x1MDAwMiNKXHUwMDE0ZYYpo3VKXFxcdTAwMGZYTSHMXHUwMDE0V9Fmm3ntyinHdSzeZjfQRHeLXHUwMDE1Ldno0f1t7+7r8WG5XSv/j11cXJrKJf8/WodcdTAwMTSpdbpcdTAwMDLMXGKIJVx1MDAwZVbMXHUwMDBls5HeXFx1mFx1MDAxOYZcdTAwMTRwt2FcdTAwMDaUY3pjfVCzXHUwMDExRKmJlmSNfu1cdTAwMDbF5JVIWoTWXGJWXHUwMDAzuGtcIomeRyS+XHUwMDE3Vqudl0rlWm2pvDaFXHUwMDFh8rz2bOJEtFxy0D5cdTAwMDJuTI9dIYlyK+irOUq1yds2v1x1MDAwNG16XHUwMDFh2CRmQFx1MDAxNlx1MDAxMMRcbpiBM5JcdTAwMDVcdTAwMWKUcIjA1Vx1MDAxMXSy2EpcckF0S0O0UlxmftIjkFx1MDAwNypcdTAwMTeIj0FCgFxmSDiWuoBEbahkOj2lL6E2+lbU5tbUj6ZcdTAwMWSow2qtVf8udv3b6ld/PlxugrQok3icXHUwMDAz/UFo+eGW02043VZ21E+nxGbZjY/zRb1cdTAwMWa5YFx1MDAwMyNcZlLEKFx1MDAxZS1RR3pcIknEkd+tXpRAkZagilx1MDAxNOFxoi741e42pps0eWdcImdcdTAwMTKEXHUwMDEwN8JoZVx1MDAwNFCE5Fx1MDAwNZM0opoorJRkUUlEdMEo11xuwpLX6TghuL7qOd0w7+LYl5tRRrmxrUa+XHUwMDE1XHUwMDA2lW7Lp55e9MRsXHUwMDEw/ZFcblx1MDAxNZyOXHUwMDFiPPz5z48je2+MxU30KVwiJnneh/T3eTWKXHUwMDFjL1EkOJ5cdTAwMDPIZ19WXHUwMDFljYrVTprgUmSUVlB6Q+LkJLt1w6hBhEJcdTAwMWNySplk9M2Wt/hsXHUwMDAyRVx1MDAxMYh3upyiW2GskvG+tTy59Py27aNcdTAwMTiQ//r3UlXKXHUwMDE0rs+rlJylL1x1MDAxMytqwnaOwVQxSMazXHUwMDAzb/L+1orWXHUwMDA2QktcdTAwMDS9KDGKZlx1MDAwNUl8SEFcdTAwMWLEhVEkOlx1MDAxN/TKQ1xuXHUwMDEzgEdcdEZSYoVcdTAwMTnoJUzFiNUvXCI5YoRcdTAwMDEzsvyxxSdUQtbGMFx1MDAwMjLPTs9cdTAwMTLFyr7TOvH6cqu822w/XFw2N1x1MDAxZZ3z8423rZfnXHUwMDE1K/MoXHUwMDAzwVxyg6mimkPZyLRK9XpcdTAwMTZcdTAwMDZES0HZy4XK5I2mrDk0OlJcdTAwMDRpXHUwMDE5XmhcdTAwMDCzpGBcdTAwMGVFYKRkw1N471xcplx1MDAwMC5ZdFTAsKfD8un7pVx1MDAwMFFmXHUwMDE0Y4Nl6uFgxz9vLFx1MDAwMKNPXHUwMDAxelx1MDAwYlI9avwxbcJcdTAwMTVhkHJm321cdTAwMThccq9cdTAwMTVPvoyKaFx1MDAxYUFKgtjUXHUwMDAw70zyXHUwMDE1nCFQPVxcgNBkmYXJRcueROJM3G3gWlExVzH4XpZl7lx1MDAwNmqibkH8uUtcdTAwMTY+U2RDXvjkTX2Z8oFcdTAwMDJzbM0ho9NcdTAwMWRcdTAwMDC/2cE3eZl4NU9nSmZcdTAwMDBdRFx1MDAxMKi2WeaoSix8XGaBXHUwMDFjXG7131Nd+2bSh1x1MDAxOIOUXHUwMDE22DBcdTAwMDI1XHUwMDA1o4pcdTAwMTexSCTUnURjXG4l0lx1MDAxOPHDuVx1MDAwMv7jYkXFz87Rdui2+4dcdTAwMWLbvavHg1x1MDAwNt1cdTAwMGZvN3bfqfjBXGLIScnon+QwJSbVZ6A1SLSWQyeJjZn0z+R124xFQDWQnomKTlVcdTAwMGLNaVH/cKRcdTAwMTmTRFx1MDAwM5njhMLfp/whSiCjwfWEXHUwMDE4XHUwMDEyXHUwMDBmJ6N+XGZcIuB6XCJcdTAwMDfbedPVz3hcYsatXHUwMDA18C1I/tDxxadcdTAwMDAsQ1Jis2//jlx1MDAwNthqZ2ClXHUwMDE1UpSDqmC4eNhCcI1cZsWYXHUwMDE3Nq1cdTAwMTabgMVMx+OBXHUwMDA0XGbEXHUwMDEz/lx1MDAxZE9abHtd+1x1MDAxZkuVPFP0Ql7yXGZcZnyZ0GF6rM5cdTAwMTGR8Dacz46yyUe8Vlx1MDAxM2VcdTAwMTLYSGGsoWRcdTAwMDRIXHSVXHUwMDA1XHUwMDE5jir55JBcdTAwMDV/I5BcdTAwMTFgKEJcdTAwMTWWUF1CYoM0O0LlXGKo0bnCQGD5fehcdTAwMDFcdTAwMDbh2VBcdTAwMDeJV56NfzONU71t7Vx1MDAxY7Q23E1v1z5o3oqT+0M9527UojROnpmna43JZ5/WsptCoJaJplx1MDAxNGzlXHUwMDAyXHUwMDE0NC/uXG5RJGT0Z708PqKm3/1yy9jwjT5CI1x1MDAwNlU8V0+nb2dYb1x1MDAwMUhcbqKMUVx1MDAxOL5CVDOVfmBcdTAwMDFcdFx1MDAwYlJcdTAwMWNi/DZcdTAwMTOL8iCAa/ZcXDg63Fc7XHUwMDE3gnpD0Z9CcfB45NnseTPBXHUwMDE1YuD1p02mV/+F0LhkaEaUeEXFQZXBkkv6Oy63lO/rdi+MgnKZqmNcbntcdTAwMTfOd1x1MDAwZY1cdTAwMWOA7cNcdTAwMTOc161er1x1MDAxNoL/hulcdTAwMTRmxmk8OSHx2fqtY99tjYqM+Fx1MDAxMz01XHUwMDA2cFx1MDAwNFx1MDAxNTvmqp9cdTAwMWZ+/lxycIktXGYifQ== PENDINGRUNNINGCANCELLEDERRORSUCCESSWorker.start()worker.cancel()Done!Exception"},{"location":"guide/workers/#worker-events","title":"Worker events","text":"

    When a worker changes state, it sends a Worker.StateChanged event to the widget where the worker was created. You can handle this message by defining an on_worker_state_changed event handler. For instance, here is how we might log the state of the worker that updates the weather:

    weather04.py
    import httpx\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True)\n    async def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{city}\"\n            async with httpx.AsyncClient() as client:\n                response = await client.get(url)\n                weather = Text.from_ansi(response.text)\n                weather_widget.update(weather)\n        else:\n            # No city, so just blank out the weather\n            weather_widget.update(\"\")\n\n    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n        \"\"\"Called when the worker state changes.\"\"\"\n        self.log(event)\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    If you run the above code with textual you should see the worker lifetime events logged in the Textual console.

    textual run weather04.py --dev\n
    "},{"location":"guide/workers/#thread-workers","title":"Thread workers","text":"

    In previous examples we used run_worker or the work decorator in conjunction with coroutines. This works well if you are using an async API like httpx, but if your API doesn't support async you may need to use threads.

    What are threads?

    Threads are a form of concurrency supplied by your Operating System. Threads allow your code to run more than a single function simultaneously.

    You can create threads by setting thread=True on the run_worker method or the work decorator. The API for thread workers is identical to async workers, but there are a few differences you need to be aware of when writing code for thread workers.

    The first difference is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

    The second difference is that you can't cancel threads in the same way as coroutines, but you can manually check if the worker was cancelled.

    Let's demonstrate thread workers by replacing httpx with urllib.request (in the standard library). The urllib module is not async aware, so we will need to use threads:

    weather05.py
    from urllib.parse import quote\nfrom urllib.request import Request, urlopen\n\nfrom rich.text import Text\n\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker, get_current_worker\n\n\nclass WeatherApp(App):\n    \"\"\"App to display the current weather.\"\"\"\n\n    CSS_PATH = \"weather.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Enter a City\")\n        with VerticalScroll(id=\"weather-container\"):\n            yield Static(id=\"weather\")\n\n    async def on_input_changed(self, message: Input.Changed) -> None:\n        \"\"\"Called when the input changes\"\"\"\n        self.update_weather(message.value)\n\n    @work(exclusive=True, thread=True)\n    def update_weather(self, city: str) -> None:\n        \"\"\"Update the weather for the given city.\"\"\"\n        weather_widget = self.query_one(\"#weather\", Static)\n        worker = get_current_worker()\n        if city:\n            # Query the network API\n            url = f\"https://wttr.in/{quote(city)}\"\n            request = Request(url)\n            request.add_header(\"User-agent\", \"CURL\")\n            response_text = urlopen(request).read().decode(\"utf-8\")\n            weather = Text.from_ansi(response_text)\n            if not worker.is_cancelled:\n                self.call_from_thread(weather_widget.update, weather)\n        else:\n            # No city, so just blank out the weather\n            if not worker.is_cancelled:\n                self.call_from_thread(weather_widget.update, \"\")\n\n    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n        \"\"\"Called when the worker state changes.\"\"\"\n        self.log(event)\n\n\nif __name__ == \"__main__\":\n    app = WeatherApp()\n    app.run()\n

    In this example, the update_weather is not asynchronous (i.e. a regular function). The @work decorator has thread=True which makes it a thread worker. Note the use of get_current_worker which the function uses to check if it has been cancelled or not.

    Important

    Textual will raise an exception if you add the work decorator to a regular function without thread=True.

    "},{"location":"guide/workers/#posting-messages","title":"Posting messages","text":"

    Most Textual functions are not thread-safe which means you will need to use call_from_thread to run them from a thread worker. An exception would be post_message which is thread-safe. If your worker needs to make multiple updates to the UI, it is a good idea to send custom messages and let the message handler update the state of the UI.

    "},{"location":"how-to/","title":"How To","text":"

    Welcome to the How To section.

    Here you will find How To articles which cover various topics at a higher level than the Guide or Reference. We will be adding more articles in the future. If there is anything you would like to see covered, open an issue in the Textual repository!

    "},{"location":"how-to/center-things/","title":"Center things","text":"

    If you have ever needed to center something in a web page, you will be glad to know it is much easier in Textual.

    This article discusses a few different ways in which things can be centered, and the differences between them.

    "},{"location":"how-to/center-things/#aligning-widgets","title":"Aligning widgets","text":"

    The align rule will center a widget relative to one or both edges. This rule is applied to a container, and will impact how the container's children are arranged. Let's see this in practice with a trivial app containing a Static widget:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's the output:

    CenterApp Hello,\u00a0World!

    The container of the widget is the screen, which has the align: center middle; rule applied. The center part tells Textual to align in the horizontal direction, and middle tells Textual to align in the vertical direction.

    The output may surprise you. The text appears to be aligned in the middle (i.e. vertical edge), but left aligned on the horizontal. This isn't a bug \u2014 I promise. Let's make a small change to reveal what is happening here. In the next example, we will add a background and a border to our text:

    Tip

    Adding a border is a very good way of visualizing layout issues, if something isn't behaving as you would expect.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\", id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    The static widget will now have a blue background and white border:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note the static widget is as wide as the screen. Since the widget is as wide as its container, there is no room for it to move in the horizontal direction.

    Info

    The align rule applies to widgets, not the text.

    In order to see the center alignment, we will have to make the widget smaller than the width of the screen. Let's set the width of the Static widget to auto, which will make the widget just wide enough to fit the content:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, World!\", id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this now, you should see the widget is aligned on both axis:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#aligning-text","title":"Aligning text","text":"

    In addition to aligning widgets, you may also want to align text. In order to demonstrate the difference, lets update the example with some longer text. We will also set the width of the widget to something smaller, to force the text to wrap.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's what it looks like with longer text:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eCould\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258eterminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u258a \u258eclassified\u00a0address.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note how the widget is centered, but the text within it is flushed to the left edge. Left aligned text is the default, but you can also center the text with the text-align rule. Let's center align the longer text by setting this rule:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        text-align: center;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this, you will see that each line of text is individually centered:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    You can also use text-align to right align text or justify the text (align to both edges).

    "},{"location":"how-to/center-things/#aligning-content","title":"Aligning content","text":"

    There is one last rule that can help us center things. The content-align rule aligns content within a widget. It treats the text as a rectangular region and positions it relative to the space inside a widget's border.

    In order to see why we might need this rule, we need to make the Static widget larger than required to fit the text. Let's set the height of the Static widget to 9 to give the content room to move:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's what it looks like with the larger widget:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Textual aligns a widget's content to the top border by default, which is why the space is below the text. We can tell Textual to align the content to the center by setting content-align: center middle;

    Note

    Strictly speaking, we only need to align the content vertically here (there is no room to move the content left or right) So we could have done content-align-vertical: middle;

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(QUOTE, id=\"hello\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this now, the content will be centered within the widget:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#aligning-multiple-widgets","title":"Aligning multiple widgets","text":"

    It's just as easy to align multiple widgets as it is a single widget. Applying align: center middle; to the parent widget (screen or other container) will align all its children.

    Let's create an example with two widgets. The following code adds two widgets with auto dimensions:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"How about a nice game\", classes=\"words\")\n        yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    This produces the following output:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    We can center both those widgets by applying the align rule as before:

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"How about a nice game\", classes=\"words\")\n        yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    Here's the output:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    Note how the widgets are aligned as if they are a single group. In other words, their position relative to each other didn't change, just their position relative to the screen.

    If you do want to center each widget independently, you can place each widget inside its own container, and set align for those containers. Textual has a builtin Center container for just this purpose.

    Let's wrap our two widgets in a Center container:

    from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Static\n\n\nclass CenterApp(App):\n    \"\"\"How to center things.\"\"\"\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            yield Static(\"How about a nice game\", classes=\"words\")\n        with Center():\n            yield Static(\"of chess?\", classes=\"words\")\n\n\nif __name__ == \"__main__\":\n    app = CenterApp()\n    app.run()\n

    If you run this, you will see that the widgets are centered relative to each other, not just the screen:

    CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    "},{"location":"how-to/center-things/#summary","title":"Summary","text":"

    Keep the following in mind when you want to center content in Textual:

    • In order to center a widget, it needs to be smaller than its container.
    • The align rule is applied to the parent of the widget you want to center (i.e. the widget's container).
    • The text-align rule aligns text on a line by line basis.
    • The content-align rule aligns content within a widget.
    • Use the Center container if you want to align multiple widgets relative to each other.
    • Add a border if the alignment isn't working as you would expect.

    If you need further help, we are here to help.

    "},{"location":"how-to/design-a-layout/","title":"Design a Layout","text":"

    This article discusses an approach you can take when designing the layout for your applications.

    Textual's layout system is flexible enough to accommodate just about any application design you could conceive of, but it may be hard to know where to start. We will go through a few tips which will help you get over the initial hurdle of designing an application layout.

    "},{"location":"how-to/design-a-layout/#tip-1-make-a-sketch","title":"Tip 1. Make a sketch","text":"

    The initial design of your application is best done with a sketch. You could use a drawing package such as Excalidraw for your sketch, but pen and paper is equally as good.

    Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction).

    For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns.

    Note

    The approach we are discussing here is applicable even if the app you want to build looks nothing like our sketch!

    Here's our sketch:

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

    It's rough, but it's all we need.

    Try in Textual-web

    "},{"location":"how-to/design-a-layout/#tip-2-work-outside-in","title":"Tip 2. Work outside in","text":"

    Like a sculpture with a block of marble, it is best to work from the outside towards the center. If your design has fixed elements (like a header, footer, or sidebar), start with those first.

    In our sketch we have a header and footer. Since these are the outermost widgets, we will begin by adding them.

    Tip

    Textual has builtin Header and Footer widgets which you could use in a real application.

    The following example defines an app, a screen, and our header and footer widgets. Since we're starting from scratch and don't have any functionality for our widgets, we are going to use the Placeholder widget to help us visualize our design.

    In a real app, we would replace these placeholders with more useful content.

    layout01.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):  # (1)!\n    pass\n\n\nclass Footer(Placeholder):  # (2)!\n    pass\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")  # (3)!\n        yield Footer(id=\"Footer\")  # (4)!\n\n\nclass LayoutApp(App):\n    def on_mount(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. The Header widget extends Placeholder.
    2. The footer widget extends Placeholder.
    3. Creates the header widget (the id will be displayed within the placeholder widget).
    4. Creates the footer widget.

    LayoutApp #Header

    "},{"location":"how-to/design-a-layout/#tip-3-apply-docks","title":"Tip 3. Apply docks","text":"

    This app works, but the header and footer don't behave as expected. We want both of these widgets to be fixed to an edge of the screen and limited in height. In Textual this is known as docking which you can apply with the dock rule.

    We will dock the header and footer to the top and bottom edges of the screen respectively, by adding a little CSS to the widget classes:

    layout02.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header #Footer

    The DEFAULT_CSS class variable is used to set CSS directly in Python code. We could define these in an external CSS file, but writing the CSS inline like this can be convenient if it isn't too complex.

    When you dock a widget, it reduces the available area for other widgets. This means that Textual will automatically compensate for the 6 additional lines reserved for the header and footer.

    "},{"location":"how-to/design-a-layout/#tip-4-use-fr-units-for-flexible-things","title":"Tip 4. Use FR Units for flexible things","text":"

    After we've added the header and footer, we want the remaining space to be used for the main interface, which will contain the columns in the sketch. This area is flexible (will change according to the size of the terminal), so how do we ensure that it takes up precisely the space needed?

    The simplest way is to use fr units. By setting both the width and height to 1fr, we are telling Textual to divide the space equally amongst the remaining widgets. There is only a single widget, so that widget will fill all of the remaining space.

    Let's make that change.

    layout03.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass ColumnsContainer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    ColumnsContainer {\n        width: 1fr;\n        height: 1fr;\n        border: solid white;\n    }\n    \"\"\"  # (1)!\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        yield ColumnsContainer(id=\"Columns\")\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. Here's where we set the width and height to 1fr. We also add a border just to illustrate the dimensions better.

    LayoutApp #Header \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502#Columns\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 #Footer

    As you can see, the central Columns area will resize with the terminal window.

    "},{"location":"how-to/design-a-layout/#tip-5-use-containers","title":"Tip 5. Use containers","text":"

    Before we add content to the Columns area, we have an opportunity to simplify. Rather than extend Placeholder for our ColumnsContainer widget, we can use one of the builtin containers. A container is simply a widget designed to contain other widgets. Containers are styled with fr units to fill the remaining space so we won't need to add any more CSS.

    Let's replace the ColumnsContainer class in the previous example with a HorizontalScroll container, which also adds an automatic horizontal scrollbar.

    layout04.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        yield HorizontalScroll()  # (1)!\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    1. The builtin container widget.

    LayoutApp #Header #Footer

    The container will appear as blank space until we add some widgets to it.

    Let's add the columns to the HorizontalScroll. A column is itself a container which will have a vertical scrollbar, so we will define our Column by subclassing VerticalScroll. In a real app, these columns will likely be added dynamically from some kind of configuration, but let's add 4 to visualize the layout.

    We will also define a Tweet placeholder and add a few to each column.

    layout05.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass Tweet(Placeholder):\n    pass\n\n\nclass Column(VerticalScroll):\n    def compose(self) -> ComposeResult:\n        for tweet_no in range(1, 20):\n            yield Tweet(id=f\"Tweet{tweet_no}\")\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        with HorizontalScroll():\n            yield Column()\n            yield Column()\n            yield Column()\n            yield Column()\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header #Tweet1#Tweet1#Tweet1#Tweet1 #Footer

    Note from the output that each Column takes a quarter of the screen width. This happens because Column extends a container which has a width of 1fr.

    It makes more sense for a column in a Twitter / Mastodon client to use a fixed width. Let's set the width of the columns to 32.

    We also want to reduce the height of each \"tweet\". In the real app, you might set the height to \"auto\" so it fits the content, but lets set it to 5 lines for now.

    Here's the final example and a reminder of the sketch.

    layout06.pyOutputSketch
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\n\n\nclass Header(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\n\n\nclass Footer(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\n\n\nclass Tweet(Placeholder):\n    DEFAULT_CSS = \"\"\"\n    Tweet {\n        height: 5;\n        width: 1fr;\n        border: tall $background;\n    }\n    \"\"\"\n\n\nclass Column(VerticalScroll):\n    DEFAULT_CSS = \"\"\"\n    Column {\n        height: 1fr;\n        width: 32;\n        margin: 0 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        for tweet_no in range(1, 20):\n            yield Tweet(id=f\"Tweet{tweet_no}\")\n\n\nclass TweetScreen(Screen):\n    def compose(self) -> ComposeResult:\n        yield Header(id=\"Header\")\n        yield Footer(id=\"Footer\")\n        with HorizontalScroll():\n            yield Column()\n            yield Column()\n            yield Column()\n            yield Column()\n\n\nclass LayoutApp(App):\n    def on_ready(self) -> None:\n        self.push_screen(TweetScreen())\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n

    LayoutApp #Header \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet1\u258e\u258a#Tweet1\u258e\u258a#Tweet1\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u2583\u2583\u258a\u258e\u2583\u2583\u258a\u258e \u258a#Tweet2\u258e\u258a#Tweet2\u258e\u258a#Tweet2\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet3\u258e\u258a#Tweet3\u258e\u258a#Tweet3\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet4\u258e\u258a#Tweet4\u258e\u258a#Tweet4\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet5\u258e\u258a#Tweet5\u258e\u258a#Tweet5\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258c #Footer

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

    A layout like this is a great starting point. In a real app, you would start replacing each of the placeholders with builtin or custom widgets.

    "},{"location":"how-to/design-a-layout/#summary","title":"Summary","text":"

    Layout is the first thing you will tackle when building a Textual app. The following tips will help you get started.

    1. Make a sketch (pen and paper is fine).
    2. Work outside in. Start with the entire space of the terminal, add the outermost content first.
    3. Dock fixed widgets. If the content doesn't move or scroll, you probably want to dock it.
    4. Make use of fr for flexible space within layouts.
    5. Use containers to contain other widgets, particularly if they scroll!

    If you need further help, we are here to help.

    "},{"location":"how-to/package-with-hatch/","title":"Package a Textual app with Hatch","text":"

    Python apps may be distributed via PyPI so they can be installed via pip. This is known as packaging. The packaging process for Textual apps is much the same as any Python library, with the additional requirement that we can launch our app from the command line.

    Tip

    An alternative to packaging your app is to turn it into a web application with textual-web.

    In this How To we will cover how to use Hatch to package an example application.

    Hatch is a build tool (a command line app to assist with packaging). You could use any build tool to package a Textual app (such as Poetry for example), but Hatch is a good choice given its large feature set and ease of use.

    Calculator example

    CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 C+/-%\u00f7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 789\u00d7 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 456- \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 123+ \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 0.= \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    This example is calculator.py taken from the examples directory in the Textual repository.

    "},{"location":"how-to/package-with-hatch/#foreword","title":"Foreword","text":"

    Packaging with Python can be a little intimidating if you haven't tackled it before. But it's not all that complicated. When you have been through it once or twice, you should find it fairly straightforward.

    "},{"location":"how-to/package-with-hatch/#example-repository","title":"Example repository","text":"

    See the textual-calculator-hatch repository for the project created in this How To.

    "},{"location":"how-to/package-with-hatch/#the-example-app","title":"The example app","text":"

    To demonstrate packaging we are going to take the calculator example from the examples directory, and publish it to PyPI. The end goal is to allow a user to install it with pip:

    pip install textual-calculator\n

    Then launch the app from the command line:

    calculator\n
    "},{"location":"how-to/package-with-hatch/#installing-hatch","title":"Installing Hatch","text":"

    There are a few ways to install Hatch. See the official docs on installation for the best method for your operating system.

    Once installed, you should have the hatch command available on the command line. Run the following to check Hatch was installed correctly:

    hatch\n
    "},{"location":"how-to/package-with-hatch/#hatch-new","title":"Hatch new","text":"

    Hatch can create an initial directory structure and files with the new subcommand. Enter hatch new followed by the name of your project. For the calculator example, the name will be \"textual calculator\":

    hatch new \"textual calculator\"\n

    This will create the following directory structure:

    textual-calculator\n\u251c\u2500\u2500 LICENSE.txt\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 src\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 textual_calculator\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 __about__.py\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 __init__.py\n\u2514\u2500\u2500 tests\n    \u2514\u2500\u2500 __init__.py\n

    This follows a well established convention when packaging Python code, and will create the following files:

    • LICENSE.txt contains the license you want to distribute your code under.
    • README.md is a markdown file containing information about your project, which will be displayed in PyPI and Github (if you use it). You can edit this with information about your app and how to use it.
    • pyproject.toml is a TOML file which contains metadata (additional information) about your project and how to package it. This is a Python standard. This file may be edited manually or by a build tool (such as Hatch).
    • src/textual_calculator/__about__.py contains the version number of your app. You should update this when you release new versions.
    • src/textual_calculator/__init__.py and tests/__init__py indicate the directory they are within contains Python code (these files are often empty).

    In the top level is a directory called src. This should contain a directory named after your project, and will be the name your code can be imported from. In our example, this directory is textual_calculator so we can do import textual_calculator in Python code.

    Additionally, there is a tests directory where you can add any test code.

    "},{"location":"how-to/package-with-hatch/#more-on-naming","title":"More on naming","text":"

    Note how Hatch replaced the space in the project name with a hyphen (i.e. textual-calculator), but the directory in src with an underscore (i.e. textual_calculator). This is because the directory in src contains the Python module, and a hyphen is not legal in a Python import. The top-level directory doesn't have this restriction and uses a hyphen, which is more typical for a directory name.

    Bear this in mind if your project name contains spaces.

    "},{"location":"how-to/package-with-hatch/#got-existing-code","title":"Got existing code?","text":"

    The hatch new command assumes you are starting from scratch. If you have existing code you would like to package, navigate to your directory and run the following command (replace <YOUR ROJECT NAME> with the name of your project):

    hatch new --init <YOUR PROJECT NAME>\n

    This will generate a pyproject.toml in the current directory.

    Note

    It will simplify things if your code follows the directory structure convention above. This may require that you move your files -- you only need to do this once!

    "},{"location":"how-to/package-with-hatch/#adding-code","title":"Adding code","text":"

    Your code should reside inside src/<PROJECT NAME>. For the calculator example we will copy calculator.py and calculator.tcss into the src/textual_calculator directory, so our directory will look like the following:

    textual-calculator\n\u251c\u2500\u2500 LICENSE.txt\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 src\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 textual_calculator\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 __about__.py\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 __init__.py\n\u2502\u00a0\u00a0     \u251c\u2500\u2500 calculator.py\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 calculator.tcss\n\u2514\u2500\u2500 tests\n    \u2514\u2500\u2500 __init__.py\n
    "},{"location":"how-to/package-with-hatch/#adding-dependencies","title":"Adding dependencies","text":"

    Your Textual app will likely depend on other Python libraries (at the very least Textual itself). We need to list these in pyproject.toml to ensure that these dependencies are installed alongside your app.

    In pyproject.toml there should be a section beginning with [project], which will look something like the following:

    [project]\nname = \"textual-calculator\"\ndynamic = [\"version\"]\ndescription = 'A example app'\nreadme = \"README.md\"\nrequires-python = \">=3.8\"\nlicense = \"MIT\"\nkeywords = []\nauthors = [\n  { name = \"Will McGugan\", email = \"redacted@textualize.io\" },\n]\nclassifiers = [\n  \"Development Status :: 4 - Beta\",\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3.8\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: Implementation :: CPython\",\n  \"Programming Language :: Python :: Implementation :: PyPy\",\n]\ndependencies = []\n

    We are interested in the dependencies value, which should list the app's dependencies. If you want a particular version of a project you can add == followed by the version.

    For the calculator, the only dependency is Textual. We can add Textual by modifying the following line:

    dependencies = [\"textual==0.47.1\"]\n

    At the time of writing, the latest Textual is 0.47.1. The entry in dependencies will ensure we get that particular version, even when newer versions are released.

    See the Hatch docs for more information on specifying dependencies.

    "},{"location":"how-to/package-with-hatch/#environments","title":"Environments","text":"

    A common problem when working with Python code is managing multiple projects with different dependencies. For instance, if we had another app that used version 0.40.0 of Textual, it may break if we installed version 0.47.1.

    The standard way of solving this is with virtual environments (or venvs), which allow each project to have its own set of dependencies. Hatch can create virtual environments for us, and makes working with them very easy.

    To create a new virtual environment, navigate to the directory with the pyproject.toml file and run the following command (this is only require once, as the virtual environment will persist):

    hatch env create\n

    Then run the following command to activate the virtual environment:

    hatch shell\n

    If you run python now, it will have our app and its dependencies available for import:

    $ python\nPython 3.11.1 (main, Jan  1 2023, 10:28:48) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin\nType \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n>>> from textual_calculator import calculator\n
    "},{"location":"how-to/package-with-hatch/#running-the-app","title":"Running the app","text":"

    You can launch the calculator from the command line with the following command:

    python -m textual_calculator.calculator\n

    The -m switch tells Python to import the module and run it.

    Although you can run your app this way (and it is fine for development), it's not ideal for sharing. It would be preferable to have a dedicated command to launch the app, so the user can easily run it from the command line. To do that, we will need to add an entry point to pyproject.toml

    "},{"location":"how-to/package-with-hatch/#entry-points","title":"Entry points","text":"

    An entry point is a function in your project that can be run from the command line. For our calculator example, we first need to create a function that will run the app. Add the following file to the src/textual_calculator folder, and name it entry_points.py:

    from textual_calculator.calculator import CalculatorApp\n\n\ndef calculator():\n    app = CalculatorApp()\n    app.run()\n

    Tip

    If you already have a function that runs your app, you may not need an entry_points.py file.

    Then edit pyproject.toml to add the following section:

    [project.scripts]\ncalculator = \"textual_calculator.entry_points:calculator\"\n

    Each entry in the [project.scripts] section (there can be more than one) maps a command on to an import and function name. In the second line above, before the = character, calculator is the name of the command. The string after the = character contains the name of the import (textual_calculator.entry_points), followed by a colon (:), and then the name of the function (also called calculator).

    Specifying an entry point like this is equivalent to doing the following from the Python REPL:

    >>> import textual_calculator.entry_points\n>>> textual_calculator.entry_points.calculator()\n

    To add the calculator command once you have edited pyproject.toml, run the following from the command line:

    pip install -e .\n

    Info

    You will have no doubt used pip before, but perhaps not with -e .. The addition of -e installs the project in editable mode which means pip won't copy the .py files code anywhere, the dot (.) indicates were installing the project in the current directory.

    Now you can launch the calculator from the command line as follows:

    calculator\n
    "},{"location":"how-to/package-with-hatch/#building","title":"Building","text":"

    Building produces archive files that contain your code. When you install a package via pip or other tool, it will download one of these archives.

    To build your project with Hatch, change to the directory containing your pyproject.toml and run the hatch build subcommand:

    cd textual-calculator\nhatch build\n

    After a moment, you should find that Hatch has created a dist (distribution) folder, which contains the project archive files. You don't typically need to use these files directly, but feel free to have a look at the directory contents.

    Packaging TCSS and other files

    Hatch will typically include all the files needed by your project, i.e. the .py files. It will also include any Textual CSS (.tcss) files in the project directory. Not all build tools will include files other than .py; if you are using another build tool, you may have to consult the documentation for how to add the Textual CSS files.

    "},{"location":"how-to/package-with-hatch/#publishing","title":"Publishing","text":"

    After your project has been successfully built you are ready to publish it to PyPI.

    If you don't have a PyPI account, you can create one now. Be sure to follow the instructions to validate your email and set up 2FA (Two Factor Authentication).

    Once you have an account, login to PyPI and go to the Account Settings tab. Scroll down and click the \"Add API token\" button. In the \"Create API Token\" form, create a token with name \"Uploads\" and select the \"Entire project\" scope, then click the \"Create token\" button.

    Copy this API token (long string of random looking characters) somewhere safe. This API token is how PyPI authenticates uploads are for your account, so you should never share your API token or upload it to the internet.

    Run the following command (replacing <YOUR API TOKEN> with the text generated in the previous step):

    hatch publish -u __token__ -a <YOUR API TOKEN>\n

    Hatch will upload the distribution files, and you should see a PyPI URL in the terminal.

    "},{"location":"how-to/package-with-hatch/#managing-api-tokens","title":"Managing API Tokens","text":"

    Creating an API token with the \"all projects\" permission is required for the first upload. You may want to generate a new API token with permissions to upload a single project when you upload a new version of your app (and delete the old one). This way if your token is leaked, it will only impact the one project.

    "},{"location":"how-to/package-with-hatch/#publishing-new-versions","title":"Publishing new versions","text":"

    If you have made changes to your app, and you want to publish the updates, you will need to update the version value in the __about__.py file, then repeat the build and publish steps.

    Managing version numbers

    See Semver for a popular versioning system (used by Textual itself).

    "},{"location":"how-to/package-with-hatch/#installing-the-calculator","title":"Installing the calculator","text":"

    From the user's point of view, they only need run the following command to install the calculator:

    pip install textual_calculator\n

    They will then be able to launch the calculator with the following command:

    calculator\n
    "},{"location":"how-to/package-with-hatch/#pipx","title":"Pipx","text":"

    A downside of installing apps this way is that unless the user has created a virtual environment, they may find it breaks other packages with conflicting dependencies.

    A good solution to this issue is pipx which automatically creates virtual environments that won't conflict with any other Python commands. Once PipX is installed, you can advise users to install your app with the following command:

    pipx install textual_calculator\n

    This will install the calculator and the textual dependency as before, but without the potential of dependency conflicts.

    "},{"location":"how-to/package-with-hatch/#summary","title":"Summary","text":"
    1. Use a build system, such as Hatch.
    2. Initialize your project with hatch new (or equivalent).
    3. Write a function to run your app, if there isn't one already.
    4. Add your dependencies and entry points to pyproject.toml.
    5. Build your app with hatch build.
    6. Publish your app with hatch publish.

    If you have any problems packaging Textual apps, we are here to help!

    "},{"location":"how-to/render-and-compose/","title":"Render and compose","text":"

    A common question that comes up on the Textual Discord server is what is the difference between render and compose methods on a widget? In this article we will clarify the differences, and use both these methods to build something fun.

    "},{"location":"how-to/render-and-compose/#which-method-to-use","title":"Which method to use?","text":"

    Render and compose are easy to confuse because they both ultimately define what a widget will look like, but they have quite different uses.

    The render method on a widget returns a Rich renderable, which is anything you could print with Rich. The simplest renderable is just text; so render() methods often return a string, but could equally return a Text instance, a Table, or anything else from Rich (or third party library). Whatever is returned from render() will be combined with any styles from CSS and displayed within the widget's borders.

    The compose method is used to build compound widgets (widgets composed of other widgets).

    A general rule of thumb, is that if you implement a compose method, there is no need for a render method because it is the widgets yielded from compose which define how the custom widget will look. However, you can mix these two methods. If you implement both, the render method will set the custom widget's background and compose will add widgets on top of that background.

    "},{"location":"how-to/render-and-compose/#combining-render-and-compose","title":"Combining render and compose","text":"

    Let's look at an example that combines both these methods. We will create a custom widget with a linear gradient as a background. The background will be animated (I did promise fun)!

    render_compose.pyOutput
    from time import time\n\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Container\nfrom textual.renderables.gradient import LinearGradient\nfrom textual.widgets import Static\n\nCOLORS = [\n    \"#881177\",\n    \"#aa3355\",\n    \"#cc6666\",\n    \"#ee9944\",\n    \"#eedd00\",\n    \"#99dd55\",\n    \"#44dd88\",\n    \"#22ccbb\",\n    \"#00bbcc\",\n    \"#0099cc\",\n    \"#3366bb\",\n    \"#663399\",\n]\nSTOPS = [(i / (len(COLORS) - 1), color) for i, color in enumerate(COLORS)]\n\n\nclass Splash(Container):\n    \"\"\"Custom widget that extends Container.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Splash {\n        align: center middle;\n    }\n    Static {\n        width: 40;\n        padding: 2 4;\n    }\n    \"\"\"\n\n    def on_mount(self) -> None:\n        self.auto_refresh = 1 / 30  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Making a splash with Textual!\")  # (2)!\n\n    def render(self) -> RenderableType:\n        return LinearGradient(time() * 90, STOPS)  # (3)!\n\n\nclass SplashApp(App):\n    \"\"\"Simple app to show our custom widget.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Splash()\n\n\nif __name__ == \"__main__\":\n    app = SplashApp()\n    app.run()\n
    1. Refresh the widget 30 times a second.
    2. Compose our compound widget, which contains a single Static.
    3. Render a linear gradient in the background.

    SplashApp \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580Making\u00a0a\u00a0splash\u00a0with\u00a0Textual!\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580

    The Splash custom widget has a compose method which adds a simple Static widget to display a message. Additionally there is a render method which returns a renderable to fill the background with a gradient.

    Tip

    As fun as this is, spinning animated gradients may be too distracting for most apps!

    "},{"location":"how-to/render-and-compose/#summary","title":"Summary","text":"

    Keep the following in mind when building custom widgets.

    1. Use render to return simple text, or a Rich renderable.
    2. Use compose to create a widget out of other widgets.
    3. If you define both, then render will be used as a background.

    We are here to help!

    "},{"location":"reference/","title":"Reference","text":"

    Welcome to the Textual Reference.

    • CSS Types

      CSS Types are the data types that CSS styles accept in their rules.

      CSS Types Reference

    • Events

      Events are how Textual communicates with your application.

      Events Reference

    • Styles

      All the styles you can use to take your Textual app to the next level.

      Styles Reference

    • Widgets

      How to use the many widgets builtin to Textual.

      Widgets Reference

    "},{"location":"styles/","title":"Styles","text":"

    A reference to Widget styles.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"styles/align/","title":"Align","text":"

    The align style aligns children within a container.

    "},{"location":"styles/align/#syntax","title":"Syntax","text":"
    \nalign: <horizontal> <vertical>;\n\nalign-horizontal: <horizontal>;\nalign-vertical: <vertical>;\n

    The align style takes a <horizontal> followed by a <vertical>.

    You can also set the alignment for each axis individually with align-horizontal and align-vertical.

    "},{"location":"styles/align/#examples","title":"Examples","text":""},{"location":"styles/align/#basic-usage","title":"Basic usage","text":"

    This example contains a simple app with two labels centered on the screen with align: center middle;:

    Outputalign.pyalign.tcss

    AlignApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Vertical\u00a0alignment\u00a0with\u00a0Textual\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Take\u00a0note,\u00a0browsers.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass AlignApp(App):\n    CSS_PATH = \"align.tcss\"\n\n    def compose(self):\n        yield Label(\"Vertical alignment with [b]Textual[/]\", classes=\"box\")\n        yield Label(\"Take note, browsers.\", classes=\"box\")\n\n\nif __name__ == \"__main__\":\n    app = AlignApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\n.box {\n    width: 40;\n    height: 5;\n    margin: 1;\n    padding: 1;\n    background: green;\n    color: white 90%;\n    border: heavy white;\n}\n
    "},{"location":"styles/align/#all-alignments","title":"All alignments","text":"

    The next example shows a 3 by 3 grid of containers with text labels. Each label has been aligned differently inside its container, and its text shows its align: ... value.

    Outputalign_all.pyalign_all.tcss

    AlignAllApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502left\u00a0top\u2502\u2502center\u00a0top\u2502\u2502right\u00a0top\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0middle\u2502\u2502center\u00a0middle\u2502\u2502right\u00a0middle\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0bottom\u2502\u2502center\u00a0bottom\u2502\u2502right\u00a0bottom\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass AlignAllApp(App):\n    \"\"\"App that illustrates all alignments.\"\"\"\n\n    CSS_PATH = \"align_all.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Container(Label(\"left top\"), id=\"left-top\")\n        yield Container(Label(\"center top\"), id=\"center-top\")\n        yield Container(Label(\"right top\"), id=\"right-top\")\n        yield Container(Label(\"left middle\"), id=\"left-middle\")\n        yield Container(Label(\"center middle\"), id=\"center-middle\")\n        yield Container(Label(\"right middle\"), id=\"right-middle\")\n        yield Container(Label(\"left bottom\"), id=\"left-bottom\")\n        yield Container(Label(\"center bottom\"), id=\"center-bottom\")\n        yield Container(Label(\"right bottom\"), id=\"right-bottom\")\n\n\nif __name__ == \"__main__\":\n    AlignAllApp().run()\n
    #left-top {\n    /* align: left top; this is the default value and is implied. */\n}\n\n#center-top {\n    align: center top;\n}\n\n#right-top {\n    align: right top;\n}\n\n#left-middle {\n    align: left middle;\n}\n\n#center-middle {\n    align: center middle;\n}\n\n#right-middle {\n    align: right middle;\n}\n\n#left-bottom {\n    align: left bottom;\n}\n\n#center-bottom {\n    align: center bottom;\n}\n\n#right-bottom {\n    align: right bottom;\n}\n\nScreen {\n    layout: grid;\n    grid-size: 3 3;\n    grid-gutter: 1;\n}\n\nContainer {\n    background: $boost;\n    border: solid gray;\n    height: 100%;\n}\n\nLabel {\n    width: auto;\n    height: 1;\n    background: $accent;\n}\n
    "},{"location":"styles/align/#css","title":"CSS","text":"
    /* Align child widgets to the center. */\nalign: center middle;\n/* Align child widget to the top right */\nalign: right top;\n\n/* Change the horizontal alignment of the children of a widget */\nalign-horizontal: right;\n/* Change the vertical alignment of the children of a widget */\nalign-vertical: middle;\n
    "},{"location":"styles/align/#python","title":"Python","text":"
    # Align child widgets to the center\nwidget.styles.align = (\"center\", \"middle\")\n# Align child widgets to the top right\nwidget.styles.align = (\"right\", \"top\")\n\n# Change the horizontal alignment of the children of a widget\nwidget.styles.align_horizontal = \"right\"\n# Change the vertical alignment of the children of a widget\nwidget.styles.align_vertical = \"middle\"\n
    "},{"location":"styles/align/#see-also","title":"See also","text":"
    • content-align to set the alignment of content inside a widget.
    • text-align to set the alignment of text in a widget.
    "},{"location":"styles/background/","title":"Background","text":"

    The background style sets the background color of a widget.

    "},{"location":"styles/background/#syntax","title":"Syntax","text":"
    \nbackground: <color> [<percentage>];\n

    The background style requires a <color> optionally followed by <percentage> to specify the color's opacity (clamped between 0% and 100%).

    "},{"location":"styles/background/#examples","title":"Examples","text":""},{"location":"styles/background/#basic-usage","title":"Basic usage","text":"

    This example creates three widgets and applies a different background to each.

    Outputbackground.pybackground.tcss

    BackgroundApp Widget\u00a01 Widget\u00a02 Widget\u00a03

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BackgroundApp(App):\n    CSS_PATH = \"background.tcss\"\n\n    def compose(self):\n        yield Label(\"Widget 1\", id=\"static1\")\n        yield Label(\"Widget 2\", id=\"static2\")\n        yield Label(\"Widget 3\", id=\"static3\")\n\n\nif __name__ == \"__main__\":\n    app = BackgroundApp()\n    app.run()\n
    Label {\n    width: 100%;\n    height: 1fr;\n    content-align: center middle;\n    color: white;\n}\n\n#static1 {\n    background: red;\n}\n\n#static2 {\n    background: rgb(0, 255, 0);\n}\n\n#static3 {\n    background: hsl(240, 100%, 50%);\n}\n
    "},{"location":"styles/background/#different-opacity-settings","title":"Different opacity settings","text":"

    The next example creates ten widgets laid out side by side to show the effect of setting different percentages for the background color's opacity.

    Outputbackground_transparency.pybackground_transparency.tcss

    BackgroundTransparencyApp 10%20%30%40%50%60%70%80%90%100%

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass BackgroundTransparencyApp(App):\n    \"\"\"Simple app to exemplify different transparency settings.\"\"\"\n\n    CSS_PATH = \"background_transparency.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"10%\", id=\"t10\")\n        yield Static(\"20%\", id=\"t20\")\n        yield Static(\"30%\", id=\"t30\")\n        yield Static(\"40%\", id=\"t40\")\n        yield Static(\"50%\", id=\"t50\")\n        yield Static(\"60%\", id=\"t60\")\n        yield Static(\"70%\", id=\"t70\")\n        yield Static(\"80%\", id=\"t80\")\n        yield Static(\"90%\", id=\"t90\")\n        yield Static(\"100%\", id=\"t100\")\n\n\nif __name__ == \"__main__\":\n    app = BackgroundTransparencyApp()\n    app.run()\n
    #t10 {\n    background: red 10%;\n}\n\n#t20 {\n    background: red 20%;\n}\n\n#t30 {\n    background: red 30%;\n}\n\n#t40 {\n    background: red 40%;\n}\n\n#t50 {\n    background: red 50%;\n}\n\n#t60 {\n    background: red 60%;\n}\n\n#t70 {\n    background: red 70%;\n}\n\n#t80 {\n    background: red 80%;\n}\n\n#t90 {\n    background: red 90%;\n}\n\n#t100 {\n    background: red 100%;\n}\n\nScreen {\n    layout: horizontal;\n}\n\nStatic {\n    height: 100%;\n    width: 1fr;\n    content-align: center middle;\n}\n
    "},{"location":"styles/background/#css","title":"CSS","text":"
    /* Blue background */\nbackground: blue;\n\n/* 20% red background */\nbackground: red 20%;\n\n/* RGB color */\nbackground: rgb(100, 120, 200);\n\n/* HSL color */\nbackground: hsl(290, 70%, 80%);\n
    "},{"location":"styles/background/#python","title":"Python","text":"

    You can use the same syntax as CSS, or explicitly set a Color object for finer-grained control.

    # Set blue background\nwidget.styles.background = \"blue\"\n# Set through HSL model\nwidget.styles.background = \"hsl(351,32%,89%)\"\n\nfrom textual.color import Color\n# Set with a color object by parsing a string\nwidget.styles.background = Color.parse(\"pink\")\nwidget.styles.background = Color.parse(\"#FF00FF\")\n# Set with a color object instantiated directly\nwidget.styles.background = Color(120, 60, 100)\n
    "},{"location":"styles/background/#see-also","title":"See also","text":"
    • color to set the color of text in a widget.
    "},{"location":"styles/border/","title":"Border","text":"

    The border style enables the drawing of a box around a widget.

    A border style may also be applied to individual edges with border-top, border-right, border-bottom, and border-left.

    Note

    border and outline cannot coexist in the same edge of a widget.

    "},{"location":"styles/border/#syntax","title":"Syntax","text":"
    \nborder: [<border>] [<color>] [<percentage>];\n\nborder-top: [<border>] [<color>] [<percentage>];\nborder-right: [<border>] [<color> [<percentage>]];\nborder-bottom: [<border>] [<color> [<percentage>]];\nborder-left: [<border>] [<color> [<percentage>]];\n

    In CSS, the border is set with a border style and a color. Both are optional. An optional percentage may be added to blend the border with the background color.

    In Python, the border is set with a tuple of border style and a color.

    "},{"location":"styles/border/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively:

    textual borders\n

    Alternatively, you can see the examples below.

    "},{"location":"styles/border/#examples","title":"Examples","text":""},{"location":"styles/border/#basic-usage","title":"Basic usage","text":"

    This examples shows three widgets with different border styles.

    Outputborder.pyborder.tcss

    BorderApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0border\u00a0is\u00a0solid\u00a0red\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0border\u00a0is\u00a0dashed\u00a0green\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0border\u00a0is\u00a0tall\u00a0blue\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderApp(App):\n    CSS_PATH = \"border.tcss\"\n\n    def compose(self):\n        yield Label(\"My border is solid red\", id=\"label1\")\n        yield Label(\"My border is dashed green\", id=\"label2\")\n        yield Label(\"My border is tall blue\", id=\"label3\")\n\n\nif __name__ == \"__main__\":\n    app = BorderApp()\n    app.run()\n
    #label1 {\n    background: red 20%;\n    color: red;\n    border: solid red;\n}\n\n#label2 {\n    background: green 20%;\n    color: green;\n    border: dashed green;\n}\n\n#label3 {\n    background: blue 20%;\n    color: blue;\n    border: tall blue;\n}\n\nScreen {\n    background: white;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border/#all-border-types","title":"All border types","text":"

    The next example shows a grid with all the available border types.

    Outputborder_all.pyborder_all.tcss

    AllBordersApp +----------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 |ascii|blank\u254fdashed\u254f\u2551double\u2551 +----------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596 \u2503heavy\u2503hidden/nonehkey\u2590inner\u258c \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u258a\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u258e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u258couter\u2590\u258apanel\u258e\u2502round\u2502\u2502solid\u2502 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258atall\u258e\u2588thick\u2588\u258fvkey\u2595\u258ewide\u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass AllBordersApp(App):\n    CSS_PATH = \"border_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"ascii\", id=\"ascii\"),\n            Label(\"blank\", id=\"blank\"),\n            Label(\"dashed\", id=\"dashed\"),\n            Label(\"double\", id=\"double\"),\n            Label(\"heavy\", id=\"heavy\"),\n            Label(\"hidden/none\", id=\"hidden\"),\n            Label(\"hkey\", id=\"hkey\"),\n            Label(\"inner\", id=\"inner\"),\n            Label(\"outer\", id=\"outer\"),\n            Label(\"panel\", id=\"panel\"),\n            Label(\"round\", id=\"round\"),\n            Label(\"solid\", id=\"solid\"),\n            Label(\"tall\", id=\"tall\"),\n            Label(\"thick\", id=\"thick\"),\n            Label(\"vkey\", id=\"vkey\"),\n            Label(\"wide\", id=\"wide\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllBordersApp()\n    app.run()\n
    #ascii {\n    border: ascii $accent;\n}\n\n#blank {\n    border: blank $accent;\n}\n\n#dashed {\n    border: dashed $accent;\n}\n\n#double {\n    border: double $accent;\n}\n\n#heavy {\n    border: heavy $accent;\n}\n\n#hidden {\n    border: hidden $accent;\n}\n\n#hkey {\n    border: hkey $accent;\n}\n\n#inner {\n    border: inner $accent;\n}\n\n#outer {\n    border: outer $accent;\n}\n\n#panel {\n    border: panel $accent;\n}\n\n#round {\n    border: round $accent;\n}\n\n#solid {\n    border: solid $accent;\n}\n\n#tall {\n    border: tall $accent;\n}\n\n#thick {\n    border: thick $accent;\n}\n\n#vkey {\n    border: vkey $accent;\n}\n\n#wide {\n    border: wide $accent;\n}\n\nGrid {\n    grid-size: 4 4;\n    align: center middle;\n    grid-gutter: 1 2;\n}\n\nLabel {\n    width: 20;\n    height: 3;\n    content-align: center middle;\n}\n
    "},{"location":"styles/border/#borders-and-outlines","title":"Borders and outlines","text":"

    The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

    This example also shows that a widget cannot contain both a border and an outline:

    Outputoutline_vs_border.pyoutline_vs_border.tcss

    OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineBorderApp(App):\n    CSS_PATH = \"outline_vs_border.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, classes=\"outline\")\n        yield Label(TEXT, classes=\"border\")\n        yield Label(TEXT, classes=\"outline border\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineBorderApp()\n    app.run()\n
    Label {\n    height: 8;\n}\n\n.outline {\n    outline: $error round;\n}\n\n.border {\n    border: $success heavy;\n}\n
    "},{"location":"styles/border/#css","title":"CSS","text":"
    /* Set a heavy white border */\nborder: heavy white;\n\n/* Set a red border on the left */\nborder-left: outer red;\n\n/* Set a rounded orange border, 50% opacity. */\nborder: round orange 50%;\n
    "},{"location":"styles/border/#python","title":"Python","text":"
    # Set a heavy white border\nwidget.styles.border = (\"heavy\", \"white\")\n\n# Set a red border on the left\nwidget.styles.border_left = (\"outer\", \"red\")\n
    "},{"location":"styles/border/#see-also","title":"See also","text":"
    • box-sizing to specify how to account for the border in a widget's dimensions.
    • outline to add an outline around the content of a widget.
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_align/","title":"Border-subtitle-align","text":"

    The border-subtitle-align style sets the horizontal alignment for the border subtitle.

    "},{"location":"styles/border_subtitle_align/#syntax","title":"Syntax","text":"
    \nborder-subtitle-align: <horizontal>;\n

    The border-subtitle-align style takes a <horizontal> that determines where the border subtitle is aligned along the top edge of the border. This means that the border corners are always visible.

    "},{"location":"styles/border_subtitle_align/#default","title":"Default","text":"

    The default alignment is right.

    "},{"location":"styles/border_subtitle_align/#examples","title":"Examples","text":""},{"location":"styles/border_subtitle_align/#basic-usage","title":"Basic usage","text":"

    This example shows three labels, each with a different border subtitle alignment:

    Outputborder_subtitle_align.pyborder_subtitle_align.tcss

    BorderSubtitleAlignApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0subtitle\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Right\u00a0>\u00a0\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderSubtitleAlignApp(App):\n    CSS_PATH = \"border_subtitle_align.tcss\"\n\n    def compose(self):\n        lbl = Label(\"My subtitle is on the left.\", id=\"label1\")\n        lbl.border_subtitle = \"< Left\"\n        yield lbl\n\n        lbl = Label(\"My subtitle is centered\", id=\"label2\")\n        lbl.border_subtitle = \"Centered!\"\n        yield lbl\n\n        lbl = Label(\"My subtitle is on the right\", id=\"label3\")\n        lbl.border_subtitle = \"Right >\"\n        yield lbl\n\n\nif __name__ == \"__main__\":\n    app = BorderSubtitleAlignApp()\n    app.run()\n
    #label1 {\n    border: solid $secondary;\n    border-subtitle-align: left;\n}\n\n#label2 {\n    border: dashed $secondary;\n    border-subtitle-align: center;\n}\n\n#label3 {\n    border: tall $secondary;\n    border-subtitle-align: right;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border_subtitle_align/#complete-usage-reference","title":"Complete usage reference","text":"

    This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

    Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

    BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\n\n\ndef make_label_container(  # (11)!\n    text: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\n    lbl = Label(text, id=id)\n    lbl.border_title = border_title\n    lbl.border_subtitle = border_subtitle\n    return Container(lbl)\n\n\nclass BorderSubTitleAlignAll(App[None]):\n    CSS_PATH = \"border_sub_title_align_all.tcss\"\n\n    def compose(self):\n        with Grid():\n            yield make_label_container(  # (1)!\n                \"This is the story of\",\n                \"lbl1\",\n                \"[b]Border [i]title[/i][/]\",\n                \"[u][r]Border[/r] subtitle[/]\",\n            )\n            yield make_label_container(  # (2)!\n                \"a Python\",\n                \"lbl2\",\n                \"[b red]Left, but it's loooooooooooong\",\n                \"[reverse]Center, but it's loooooooooooong\",\n            )\n            yield make_label_container(  # (3)!\n                \"developer that\",\n                \"lbl3\",\n                \"[b i on purple]Left[/]\",\n                \"[r u white on black]@@@[/]\",\n            )\n            yield make_label_container(\n                \"had to fill up\",\n                \"lbl4\",\n                \"\",  # (4)!\n                \"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n            )\n            yield make_label_container(  # (6)!\n                \"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n            )\n            yield make_label_container(  # (7)!\n                \"and ended up redoing it\",\n                \"lbl6\",\n                \"Title\",\n                \"Subtitle\",\n            )\n            yield make_label_container(  # (8)!\n                \"because the first try\",\n                \"lbl7\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (9)!\n                \"had some labels\",\n                \"lbl8\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (10)!\n                \"that were too long.\",\n                \"lbl9\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n\n\nif __name__ == \"__main__\":\n    app = BorderSubTitleAlignAll()\n    app.run()\n
    1. Border (sub)titles can contain nested markup.
    2. Long (sub)titles get truncated and occupy as much space as possible.
    3. (Sub)titles can be stylised with Rich markup.
    4. An empty (sub)title isn't displayed.
    5. The markup can even contain Rich links.
    6. If the widget does not have a border, the title and subtitle are not shown.
    7. When the side borders are not set, the (sub)title will align with the edge of the widget.
    8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
    9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
    10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
    11. An auxiliary function to create labels with border title and subtitle.
    Grid {\n    grid-size: 3 3;\n    align: center middle;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n    align: center middle;\n}\n\n#lbl1 {  /* (1)! */\n    border: vkey $secondary;\n}\n\n#lbl2 {  /* (2)! */\n    border: round $secondary;\n    border-title-align: right;\n    border-subtitle-align: right;\n}\n\n#lbl3 {\n    border: wide $secondary;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl4 {\n    border: ascii $success;\n    border-title-align: center;  /* (3)! */\n    border-subtitle-align: left;\n}\n\n#lbl5 {  /* (4)! */\n    /* No border = no (sub)title. */\n    border: none $success;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl6 {  /* (5)! */\n    border-top: solid $success;\n    border-bottom: solid $success;\n}\n\n#lbl7 {  /* (6)! */\n    border-top: solid $error;\n    border-bottom: solid $error;\n    padding: 1 2;\n    border-subtitle-align: left;\n}\n\n#lbl8 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl9 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: right;\n}\n
    1. The default alignment for the title is left and the default alignment for the subtitle is right.
    2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
    3. Setting the alignment does not affect empty (sub)titles.
    4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
    5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
    6. Naturally, (sub)title positioning is affected by padding.
    "},{"location":"styles/border_subtitle_align/#css","title":"CSS","text":"
    border-subtitle-align: left;\nborder-subtitle-align: center;\nborder-subtitle-align: right;\n
    "},{"location":"styles/border_subtitle_align/#python","title":"Python","text":"
    widget.styles.border_subtitle_align = \"left\"\nwidget.styles.border_subtitle_align = \"center\"\nwidget.styles.border_subtitle_align = \"right\"\n
    "},{"location":"styles/border_subtitle_align/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_background/","title":"Border-subtitle-background","text":"

    The border-subtitle-background style sets the background color of the border_subtitle.

    "},{"location":"styles/border_subtitle_background/#syntax","title":"Syntax","text":"
    \nborder-subtitle-background: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_subtitle_background/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_background/#css","title":"CSS","text":"
    border-subtitle-background: blue;\n
    "},{"location":"styles/border_subtitle_background/#python","title":"Python","text":"
    widget.styles.border_subtitle_background = \"blue\"\n
    "},{"location":"styles/border_subtitle_background/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_color/","title":"Border-subtitle-color","text":"

    The border-subtitle-color style sets the color of the border_subtitle.

    "},{"location":"styles/border_subtitle_color/#syntax","title":"Syntax","text":"
    \nborder-subtitle-color: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_subtitle_color/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_color/#css","title":"CSS","text":"
    border-subtitle-color: red;\n
    "},{"location":"styles/border_subtitle_color/#python","title":"Python","text":"
    widget.styles.border_subtitle_color = \"red\"\n
    "},{"location":"styles/border_subtitle_color/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_subtitle_style/","title":"Border-subtitle-style","text":"

    The border-subtitle-style style sets the text style of the border_subtitle.

    "},{"location":"styles/border_subtitle_style/#syntax","title":"Syntax","text":"
    \nborder-subtitle-style: <text-style>;\n
    "},{"location":"styles/border_subtitle_style/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_subtitle_style/#css","title":"CSS","text":"
    border-subtitle-style: bold underline;\n
    "},{"location":"styles/border_subtitle_style/#python","title":"Python","text":"
    widget.styles.border_subtitle_style = \"bold underline\"\n
    "},{"location":"styles/border_subtitle_style/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_align/","title":"Border-title-align","text":"

    The border-title-align style sets the horizontal alignment for the border title.

    "},{"location":"styles/border_title_align/#syntax","title":"Syntax","text":"
    \nborder-title-align: <horizontal>;\n

    The border-title-align style takes a <horizontal> that determines where the border title is aligned along the top edge of the border. This means that the border corners are always visible.

    "},{"location":"styles/border_title_align/#default","title":"Default","text":"

    The default alignment is left.

    "},{"location":"styles/border_title_align/#examples","title":"Examples","text":""},{"location":"styles/border_title_align/#basic-usage","title":"Basic usage","text":"

    This example shows three labels, each with a different border title alignment:

    Outputborder_title_align.pyborder_title_align.tcss

    BorderTitleAlignApp \u250c\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0title\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0title\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u00a0Right\u00a0>\u00a0\u2594\u258e \u258a\u258e \u258aMy\u00a0title\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass BorderTitleAlignApp(App):\n    CSS_PATH = \"border_title_align.tcss\"\n\n    def compose(self):\n        lbl = Label(\"My title is on the left.\", id=\"label1\")\n        lbl.border_title = \"< Left\"\n        yield lbl\n\n        lbl = Label(\"My title is centered\", id=\"label2\")\n        lbl.border_title = \"Centered!\"\n        yield lbl\n\n        lbl = Label(\"My title is on the right\", id=\"label3\")\n        lbl.border_title = \"Right >\"\n        yield lbl\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleAlignApp()\n    app.run()\n
    #label1 {\n    border: solid $secondary;\n    border-title-align: left;\n}\n\n#label2 {\n    border: dashed $secondary;\n    border-title-align: center;\n}\n\n#label3 {\n    border: tall $secondary;\n    border-title-align: right;\n}\n\nScreen > Label {\n    width: 100%;\n    height: 5;\n    content-align: center middle;\n    color: white;\n    margin: 1;\n    box-sizing: border-box;\n}\n
    "},{"location":"styles/border_title_align/#complete-usage-reference","title":"Complete usage reference","text":"

    This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

    Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

    BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\n\n\ndef make_label_container(  # (11)!\n    text: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\n    lbl = Label(text, id=id)\n    lbl.border_title = border_title\n    lbl.border_subtitle = border_subtitle\n    return Container(lbl)\n\n\nclass BorderSubTitleAlignAll(App[None]):\n    CSS_PATH = \"border_sub_title_align_all.tcss\"\n\n    def compose(self):\n        with Grid():\n            yield make_label_container(  # (1)!\n                \"This is the story of\",\n                \"lbl1\",\n                \"[b]Border [i]title[/i][/]\",\n                \"[u][r]Border[/r] subtitle[/]\",\n            )\n            yield make_label_container(  # (2)!\n                \"a Python\",\n                \"lbl2\",\n                \"[b red]Left, but it's loooooooooooong\",\n                \"[reverse]Center, but it's loooooooooooong\",\n            )\n            yield make_label_container(  # (3)!\n                \"developer that\",\n                \"lbl3\",\n                \"[b i on purple]Left[/]\",\n                \"[r u white on black]@@@[/]\",\n            )\n            yield make_label_container(\n                \"had to fill up\",\n                \"lbl4\",\n                \"\",  # (4)!\n                \"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n            )\n            yield make_label_container(  # (6)!\n                \"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n            )\n            yield make_label_container(  # (7)!\n                \"and ended up redoing it\",\n                \"lbl6\",\n                \"Title\",\n                \"Subtitle\",\n            )\n            yield make_label_container(  # (8)!\n                \"because the first try\",\n                \"lbl7\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (9)!\n                \"had some labels\",\n                \"lbl8\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n            yield make_label_container(  # (10)!\n                \"that were too long.\",\n                \"lbl9\",\n                \"Title, but really loooooooooong!\",\n                \"Subtitle, but really loooooooooong!\",\n            )\n\n\nif __name__ == \"__main__\":\n    app = BorderSubTitleAlignAll()\n    app.run()\n
    1. Border (sub)titles can contain nested markup.
    2. Long (sub)titles get truncated and occupy as much space as possible.
    3. (Sub)titles can be stylised with Rich markup.
    4. An empty (sub)title isn't displayed.
    5. The markup can even contain Rich links.
    6. If the widget does not have a border, the title and subtitle are not shown.
    7. When the side borders are not set, the (sub)title will align with the edge of the widget.
    8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
    9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
    10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
    11. An auxiliary function to create labels with border title and subtitle.
    Grid {\n    grid-size: 3 3;\n    align: center middle;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n    align: center middle;\n}\n\n#lbl1 {  /* (1)! */\n    border: vkey $secondary;\n}\n\n#lbl2 {  /* (2)! */\n    border: round $secondary;\n    border-title-align: right;\n    border-subtitle-align: right;\n}\n\n#lbl3 {\n    border: wide $secondary;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl4 {\n    border: ascii $success;\n    border-title-align: center;  /* (3)! */\n    border-subtitle-align: left;\n}\n\n#lbl5 {  /* (4)! */\n    /* No border = no (sub)title. */\n    border: none $success;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl6 {  /* (5)! */\n    border-top: solid $success;\n    border-bottom: solid $success;\n}\n\n#lbl7 {  /* (6)! */\n    border-top: solid $error;\n    border-bottom: solid $error;\n    padding: 1 2;\n    border-subtitle-align: left;\n}\n\n#lbl8 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: center;\n    border-subtitle-align: center;\n}\n\n#lbl9 {\n    border-top: solid $error;\n    border-bottom: solid $error;\n    border-title-align: right;\n}\n
    1. The default alignment for the title is left and the default alignment for the subtitle is right.
    2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
    3. Setting the alignment does not affect empty (sub)titles.
    4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
    5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
    6. Naturally, (sub)title positioning is affected by padding.
    "},{"location":"styles/border_title_align/#css","title":"CSS","text":"
    border-title-align: left;\nborder-title-align: center;\nborder-title-align: right;\n
    "},{"location":"styles/border_title_align/#python","title":"Python","text":"
    widget.styles.border_title_align = \"left\"\nwidget.styles.border_title_align = \"center\"\nwidget.styles.border_title_align = \"right\"\n
    "},{"location":"styles/border_title_align/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_background/","title":"Border-title-background","text":"

    The border-title-background style sets the background color of the border_title.

    "},{"location":"styles/border_title_background/#syntax","title":"Syntax","text":"
    \nborder-title-background: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_title_background/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_background/#css","title":"CSS","text":"
    border-title-background: blue;\n
    "},{"location":"styles/border_title_background/#python","title":"Python","text":"
    widget.styles.border_title_background = \"blue\"\n
    "},{"location":"styles/border_title_background/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_color/","title":"Border-title-color","text":"

    The border-title-color style sets the color of the border_title.

    "},{"location":"styles/border_title_color/#syntax","title":"Syntax","text":"
    \nborder-title-color: (<color> | auto) [<percentage>];\n
    "},{"location":"styles/border_title_color/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_color/#css","title":"CSS","text":"
    border-title-color: red;\n
    "},{"location":"styles/border_title_color/#python","title":"Python","text":"
    widget.styles.border_title_color = \"red\"\n
    "},{"location":"styles/border_title_color/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/border_title_style/","title":"Border-title-style","text":"

    The border-title-style style sets the text style of the border_title.

    "},{"location":"styles/border_title_style/#syntax","title":"Syntax","text":"
    \nborder-title-style: <text-style>;\n
    "},{"location":"styles/border_title_style/#example","title":"Example","text":"

    The following examples demonstrates customization of the border color and text style rules.

    Outputborder_title_colors.pyborder_title_colors.tcss

    BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass BorderTitleApp(App):\n    CSS_PATH = \"border_title_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, World!\")\n\n    def on_mount(self) -> None:\n        label = self.query_one(Label)\n        label.border_title = \"Textual Rocks\"\n        label.border_subtitle = \"Textual Rocks\"\n\n\nif __name__ == \"__main__\":\n    app = BorderTitleApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nLabel {\n    padding: 4 8;\n    border: heavy red;\n\n    border-title-color: green;\n    border-title-background: white;\n    border-title-style: bold;\n\n    border-subtitle-color: magenta;\n    border-subtitle-background: yellow;\n    border-subtitle-style: italic;\n}\n
    "},{"location":"styles/border_title_style/#css","title":"CSS","text":"
    border-title-style: bold underline;\n
    "},{"location":"styles/border_title_style/#python","title":"Python","text":"
    widget.styles.border_title_style = \"bold underline\"\n
    "},{"location":"styles/border_title_style/#see-also","title":"See also","text":"
    • border-title-align to set the title's alignment.
    • border-title-color to set the title's color.
    • border-title-background to set the title's background color.
    • border-title-style to set the title's text style.

    • border-subtitle-align to set the sub-title's alignment.

    • border-subtitle-color to set the sub-title's color.
    • border-subtitle-background to set the sub-title's background color.
    • border-subtitle-style to set the sub-title's text style.
    "},{"location":"styles/box_sizing/","title":"Box-sizing","text":"

    The box-sizing style determines how the width and height of a widget are calculated.

    "},{"location":"styles/box_sizing/#syntax","title":"Syntax","text":"
    box-sizing: border-box | content-box;\n
    "},{"location":"styles/box_sizing/#values","title":"Values","text":"Value Description border-box (default) Padding and border are included in the width and height. If you add padding and/or border the widget will not change in size, but you will have less space for content. content-box Padding and border will increase the size of the widget, leaving the content area unaffected."},{"location":"styles/box_sizing/#example","title":"Example","text":"

    Both widgets in this example have the same height (5). The top widget has box-sizing: border-box which means that padding and border reduce the space for content. The bottom widget has box-sizing: content-box which increases the size of the widget to compensate for padding and border.

    Outputbox_sizing.pybox_sizing.tcss

    BoxSizingApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0border-box!\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0content-box!\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass BoxSizingApp(App):\n    CSS_PATH = \"box_sizing.tcss\"\n\n    def compose(self):\n        yield Static(\"I'm using border-box!\", id=\"static1\")\n        yield Static(\"I'm using content-box!\", id=\"static2\")\n\n\nif __name__ == \"__main__\":\n    app = BoxSizingApp()\n    app.run()\n
    #static1 {\n    box-sizing: border-box;\n}\n\n#static2 {\n    box-sizing: content-box;\n}\n\nScreen {\n    background: white;\n    color: black;\n}\n\nApp Static {\n    background: blue 20%;\n    height: 5;\n    margin: 2;\n    padding: 1;\n    border: wide black;\n}\n
    "},{"location":"styles/box_sizing/#css","title":"CSS","text":"
    /* Set box sizing to border-box (default) */\nbox-sizing: border-box;\n\n/* Set box sizing to content-box */\nbox-sizing: content-box;\n
    "},{"location":"styles/box_sizing/#python","title":"Python","text":"
    # Set box sizing to border-box (default)\nwidget.box_sizing = \"border-box\"\n\n# Set box sizing to content-box\nwidget.box_sizing = \"content-box\"\n
    "},{"location":"styles/box_sizing/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    • padding to add spacing around the content of a widget.
    "},{"location":"styles/color/","title":"Color","text":"

    The color style sets the text color of a widget.

    "},{"location":"styles/color/#syntax","title":"Syntax","text":"
    \ncolor: (<color> | auto) [<percentage>];\n

    The color style requires a <color> followed by an optional <percentage> to specify the color's opacity.

    You can also use the special value of \"auto\" in place of a color. This tells Textual to automatically select either white or black text for best contrast against the background.

    "},{"location":"styles/color/#examples","title":"Examples","text":""},{"location":"styles/color/#basic-usage","title":"Basic usage","text":"

    This example sets a different text color for each of three different widgets.

    Outputcolor.pycolor.tcss

    ColorApp I'm\u00a0red! I'm\u00a0rgb(0,\u00a0255,\u00a00)! I'm\u00a0hsl(240,\u00a0100%,\u00a050%)!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ColorApp(App):\n    CSS_PATH = \"color.tcss\"\n\n    def compose(self):\n        yield Label(\"I'm red!\", id=\"label1\")\n        yield Label(\"I'm rgb(0, 255, 0)!\", id=\"label2\")\n        yield Label(\"I'm hsl(240, 100%, 50%)!\", id=\"label3\")\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n
    Label {\n    height: 1fr;\n    content-align: center middle;\n    width: 100%;\n}\n\n#label1 {\n    color: red;\n}\n\n#label2 {\n    color: rgb(0, 255, 0);\n}\n\n#label3 {\n    color: hsl(240, 100%, 50%);\n}\n
    "},{"location":"styles/color/#auto","title":"Auto","text":"

    The next example shows how auto chooses between a lighter or a darker text color to increase the contrast and improve readability.

    Outputcolor_auto.pycolor_auto.tcss

    ColorApp The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ColorApp(App):\n    CSS_PATH = \"color_auto.tcss\"\n\n    def compose(self):\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl1\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl2\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl3\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl4\")\n        yield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl5\")\n\n\nif __name__ == \"__main__\":\n    app = ColorApp()\n    app.run()\n
    Label {\n    color: auto 80%;\n    content-align: center middle;\n    height: 1fr;\n    width: 100%;\n}\n\n#lbl1 {\n    background: red 80%;\n}\n\n#lbl2 {\n    background: yellow 80%;\n}\n\n#lbl3 {\n    background: blue 80%;\n}\n\n#lbl4 {\n    background: pink 80%;\n}\n\n#lbl5 {\n    background: green 80%;\n}\n
    "},{"location":"styles/color/#css","title":"CSS","text":"
    /* Blue text */\ncolor: blue;\n\n/* 20% red text */\ncolor: red 20%;\n\n/* RGB color */\ncolor: rgb(100, 120, 200);\n\n/* Automatically choose color with suitable contrast for readability */\ncolor: auto;\n
    "},{"location":"styles/color/#python","title":"Python","text":"

    You can use the same syntax as CSS, or explicitly set a Color object.

    # Set blue text\nwidget.styles.color = \"blue\"\n\nfrom textual.color import Color\n# Set with a color object\nwidget.styles.color = Color.parse(\"pink\")\n
    "},{"location":"styles/color/#see-also","title":"See also","text":"
    • background to set the background color in a widget.
    "},{"location":"styles/content_align/","title":"Content-align","text":"

    The content-align style aligns content inside a widget.

    "},{"location":"styles/content_align/#syntax","title":"Syntax","text":"
    \ncontent-align: <horizontal> <vertical>;\n\ncontent-align-horizontal: <horizontal>;\ncontent-align-vertical: <vertical>;\n

    The content-align style takes a <horizontal> followed by a <vertical>.

    You can specify the alignment of content on both the horizontal and vertical axes at the same time, or on each of the axis separately. To specify content alignment on a single axis, use the respective style and type:

    • content-align-horizontal takes a <horizontal> and does alignment along the horizontal axis; and
    • content-align-vertical takes a <vertical> and does alignment along the vertical axis.
    "},{"location":"styles/content_align/#examples","title":"Examples","text":""},{"location":"styles/content_align/#basic-usage","title":"Basic usage","text":"

    This first example shows three labels stacked vertically, each with different content alignments.

    Outputcontent_align.pycontent_align.tcss

    ContentAlignApp With\u00a0content-align\u00a0you\u00a0can... ...Easily\u00a0align\u00a0content... ...Horizontally\u00a0and\u00a0vertically!

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass ContentAlignApp(App):\n    CSS_PATH = \"content_align.tcss\"\n\n    def compose(self):\n        yield Label(\"With [i]content-align[/] you can...\", id=\"box1\")\n        yield Label(\"...[b]Easily align content[/]...\", id=\"box2\")\n        yield Label(\"...Horizontally [i]and[/] vertically!\", id=\"box3\")\n\n\nif __name__ == \"__main__\":\n    app = ContentAlignApp()\n    app.run()\n
    #box1 {\n    content-align: left top;\n    background: red;\n}\n\n#box2 {\n    content-align-horizontal: center;\n    content-align-vertical: middle;\n    background: green;\n}\n\n#box3 {\n    content-align: right bottom;\n    background: blue;\n}\n\nLabel {\n    width: 100%;\n    height: 1fr;\n    padding: 1;\n    color: white;\n}\n
    "},{"location":"styles/content_align/#all-content-alignments","title":"All content alignments","text":"

    The next example shows a 3 by 3 grid of labels. Each label has its text aligned differently.

    Outputcontent_align_all.pycontent_align_all.tcss

    AllContentAlignApp left\u00a0topcenter\u00a0topright\u00a0top left\u00a0middlecenter\u00a0middleright\u00a0middle left\u00a0bottomcenter\u00a0bottomright\u00a0bottom

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass AllContentAlignApp(App):\n    CSS_PATH = \"content_align_all.tcss\"\n\n    def compose(self):\n        yield Label(\"left top\", id=\"left-top\")\n        yield Label(\"center top\", id=\"center-top\")\n        yield Label(\"right top\", id=\"right-top\")\n        yield Label(\"left middle\", id=\"left-middle\")\n        yield Label(\"center middle\", id=\"center-middle\")\n        yield Label(\"right middle\", id=\"right-middle\")\n        yield Label(\"left bottom\", id=\"left-bottom\")\n        yield Label(\"center bottom\", id=\"center-bottom\")\n        yield Label(\"right bottom\", id=\"right-bottom\")\n\n\nif __name__ == \"__main__\":\n    app = AllContentAlignApp()\n    app.run()\n
    #left-top {\n    /* content-align: left top; this is the default implied value. */\n}\n#center-top {\n    content-align: center top;\n}\n#right-top {\n    content-align: right top;\n}\n#left-middle {\n    content-align: left middle;\n}\n#center-middle {\n    content-align: center middle;\n}\n#right-middle {\n    content-align: right middle;\n}\n#left-bottom {\n    content-align: left bottom;\n}\n#center-bottom {\n    content-align: center bottom;\n}\n#right-bottom {\n    content-align: right bottom;\n}\n\nScreen {\n    layout: grid;\n    grid-size: 3 3;\n    grid-gutter: 1;\n}\n\nLabel {\n    width: 100%;\n    height: 100%;\n    background: $primary;\n}\n
    "},{"location":"styles/content_align/#css","title":"CSS","text":"
    /* Align content in the very center of a widget */\ncontent-align: center middle;\n/* Align content at the top right of a widget */\ncontent-align: right top;\n\n/* Change the horizontal alignment of the content of a widget */\ncontent-align-horizontal: right;\n/* Change the vertical alignment of the content of a widget */\ncontent-align-vertical: middle;\n
    "},{"location":"styles/content_align/#python","title":"Python","text":"
    # Align content in the very center of a widget\nwidget.styles.content_align = (\"center\", \"middle\")\n# Align content at the top right of a widget\nwidget.styles.content_align = (\"right\", \"top\")\n\n# Change the horizontal alignment of the content of a widget\nwidget.styles.content_align_horizontal = \"right\"\n# Change the vertical alignment of the content of a widget\nwidget.styles.content_align_vertical = \"middle\"\n
    "},{"location":"styles/content_align/#see-also","title":"See also","text":"
    • align to set the alignment of children widgets inside a container.
    • text-align to set the alignment of text in a widget.
    "},{"location":"styles/display/","title":"Display","text":"

    The display style defines whether a widget is displayed or not.

    "},{"location":"styles/display/#syntax","title":"Syntax","text":"
    display: block | none;\n
    "},{"location":"styles/display/#values","title":"Values","text":"Value Description block (default) Display the widget as normal. none The widget is not displayed and space will no longer be reserved for it."},{"location":"styles/display/#example","title":"Example","text":"

    Note that the second widget is hidden by adding the \"remove\" class which sets the display style to none.

    Outputdisplay.pydisplay.tcss

    DisplayApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass DisplayApp(App):\n    CSS_PATH = \"display.tcss\"\n\n    def compose(self):\n        yield Static(\"Widget 1\")\n        yield Static(\"Widget 2\", classes=\"remove\")\n        yield Static(\"Widget 3\")\n\n\nif __name__ == \"__main__\":\n    app = DisplayApp()\n    app.run()\n
    Screen {\n    background: green;\n}\n\nStatic {\n    height: 5;\n    background: white;\n    color: blue;\n    border: heavy blue;\n}\n\nStatic.remove {\n    display: none;\n}\n
    "},{"location":"styles/display/#css","title":"CSS","text":"
    /* Widget is shown */\ndisplay: block;\n\n/* Widget is not shown */\ndisplay: none;\n
    "},{"location":"styles/display/#python","title":"Python","text":"
    # Hide the widget\nself.styles.display = \"none\"\n\n# Show the widget again\nself.styles.display = \"block\"\n

    There is also a shortcut to show / hide a widget. The display property on Widget may be set to True or False to show or hide the widget.

    # Hide the widget\nwidget.display = False\n\n# Show the widget\nwidget.display = True\n
    "},{"location":"styles/display/#see-also","title":"See also","text":"
    • visibility to specify whether a widget is visible or not.
    "},{"location":"styles/dock/","title":"Dock","text":"

    The dock style is used to fix a widget to the edge of a container (which may be the entire terminal window).

    "},{"location":"styles/dock/#syntax","title":"Syntax","text":"
    dock: bottom | left | right | top;\n

    The option chosen determines the edge to which the widget is docked.

    "},{"location":"styles/dock/#examples","title":"Examples","text":""},{"location":"styles/dock/#basic-usage","title":"Basic usage","text":"

    The example below shows a left docked sidebar. Notice that even though the content is scrolled, the sidebar remains fixed.

    Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

    DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\n\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\n\"\"\"\n\n\nclass DockLayoutExample(App):\n    CSS_PATH = \"dock_layout1_sidebar.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"Sidebar\", id=\"sidebar\")\n        yield Static(TEXT * 10, id=\"body\")\n\n\nif __name__ == \"__main__\":\n    app = DockLayoutExample()\n    app.run()\n
    #sidebar {\n    dock: left;\n    width: 15;\n    height: 100%;\n    color: #0f2b41;\n    background: dodgerblue;\n}\n
    "},{"location":"styles/dock/#advanced-usage","title":"Advanced usage","text":"

    The second example shows how one can use full-width or full-height containers to dock labels to the edges of a larger container. The labels will remain in that position (docked) even if the container they are in scrolls horizontally and/or vertically.

    Outputdock_all.pydock_all.tcss

    DockAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502top\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502leftright\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502bottom\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass DockAllApp(App):\n    CSS_PATH = \"dock_all.tcss\"\n\n    def compose(self):\n        yield Container(\n            Container(Label(\"left\"), id=\"left\"),\n            Container(Label(\"top\"), id=\"top\"),\n            Container(Label(\"right\"), id=\"right\"),\n            Container(Label(\"bottom\"), id=\"bottom\"),\n            id=\"big_container\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = DockAllApp()\n    app.run()\n
    #left {\n    dock: left;\n    height: 100%;\n    width: auto;\n    align-vertical: middle;\n}\n#top {\n    dock: top;\n    height: auto;\n    width: 100%;\n    align-horizontal: center;\n}\n#right {\n    dock: right;\n    height: 100%;\n    width: auto;\n    align-vertical: middle;\n}\n#bottom {\n    dock: bottom;\n    height: auto;\n    width: 100%;\n    align-horizontal: center;\n}\n\nScreen {\n    align: center middle;\n}\n\n#big_container {\n    width: 75%;\n    height: 75%;\n    border: round white;\n}\n
    "},{"location":"styles/dock/#css","title":"CSS","text":"
    dock: bottom;  /* Docks on the bottom edge of the parent container. */\ndock: left;    /* Docks on the   left edge of the parent container. */\ndock: right;   /* Docks on the  right edge of the parent container. */\ndock: top;     /* Docks on the    top edge of the parent container. */\n
    "},{"location":"styles/dock/#python","title":"Python","text":"
    widget.styles.dock = \"bottom\"  # Dock bottom.\nwidget.styles.dock = \"left\"    # Dock   left.\nwidget.styles.dock = \"right\"   # Dock  right.\nwidget.styles.dock = \"top\"     # Dock    top.\n
    "},{"location":"styles/dock/#see-also","title":"See also","text":"
    • The layout guide section on docking.
    "},{"location":"styles/height/","title":"Height","text":"

    The height style sets a widget's height.

    "},{"location":"styles/height/#syntax","title":"Syntax","text":"
    \nheight: <scalar>;\n

    The height style needs a <scalar> to determine the vertical length of the widget. By default, it sets the height of the content area, but if box-sizing is set to border-box it sets the height of the border area.

    "},{"location":"styles/height/#examples","title":"Examples","text":""},{"location":"styles/height/#basic-usage","title":"Basic usage","text":"

    This examples creates a widget with a height of 50% of the screen.

    Outputheight.pyheight.tcss

    HeightApp Widget

    from textual.app import App\nfrom textual.widget import Widget\n\n\nclass HeightApp(App):\n    CSS_PATH = \"height.tcss\"\n\n    def compose(self):\n        yield Widget()\n\n\nif __name__ == \"__main__\":\n    app = HeightApp()\n    app.run()\n
    Screen > Widget {\n    background: green;\n    height: 50%;\n    color: white;\n}\n
    "},{"location":"styles/height/#all-height-formats","title":"All height formats","text":"

    The next example creates a series of wide widgets with heights set with different units. Open the CSS file tab to see the comments that explain how each height is computed. (The output includes a vertical ruler on the right to make it easier to check the height of each widget.)

    Outputheight_comparison.pyheight_comparison.tcss

    HeightComparisonApp #cells\u00b7 \u00b7 \u00b7 #percent\u00b7 \u2022 \u00b7 #w\u00b7 \u00b7 \u00b7 \u2022 #h\u00b7 \u00b7 \u00b7 \u00b7 #vw\u2022 \u00b7 \u00b7 \u00b7 #vh\u00b7 \u2022 #auto\u00b7 #fr1\u00b7 #fr2\u00b7 \u00b7

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Label, Placeholder, Static\n\n\nclass Ruler(Static):\n    def compose(self):\n        ruler_text = \"\u00b7\\n\u00b7\\n\u00b7\\n\u00b7\\n\u2022\\n\" * 100\n        yield Label(ruler_text)\n\n\nclass HeightComparisonApp(App):\n    CSS_PATH = \"height_comparison.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(id=\"cells\"),  # (1)!\n            Placeholder(id=\"percent\"),\n            Placeholder(id=\"w\"),\n            Placeholder(id=\"h\"),\n            Placeholder(id=\"vw\"),\n            Placeholder(id=\"vh\"),\n            Placeholder(id=\"auto\"),\n            Placeholder(id=\"fr1\"),\n            Placeholder(id=\"fr2\"),\n        )\n        yield Ruler()\n\n\nif __name__ == \"__main__\":\n    app = HeightComparisonApp()\n    app.run()\n
    1. The id of the placeholder identifies which unit will be used to set the height of the widget.
    #cells {\n    height: 2;       /* (1)! */\n}\n#percent {\n    height: 12.5%;   /* (2)! */\n}\n#w {\n    height: 5w;      /* (3)! */\n}\n#h {\n    height: 12.5h;   /* (4)! */\n}\n#vw {\n    height: 6.25vw;  /* (5)! */\n}\n#vh {\n    height: 12.5vh;  /* (6)! */\n}\n#auto {\n    height: auto;    /* (7)! */\n}\n#fr1 {\n    height: 1fr;     /* (8)! */\n}\n#fr2 {\n    height: 2fr;     /* (9)! */\n}\n\nScreen {\n    layers: ruler;\n    overflow: hidden;\n}\n\nRuler {\n    layer: ruler;\n    dock: right;\n    width: 1;\n    background: $accent;\n}\n
    1. This sets the height to 2 lines.
    2. This sets the height to 12.5% of the space made available by the container. The container is 24 lines tall, so 12.5% of 24 is 3.
    3. This sets the height to 5% of the width of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the width of the VerticalScroll is 80 and 5% of 80 is 4.
    4. This sets the height to 12.5% of the height of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the height of the VerticalScroll is 24 and 12.5% of 24 is 3.
    5. This sets the height to 6.25% of the viewport width, which is 80. 6.25% of 80 is 5.
    6. This sets the height to 12.5% of the viewport height, which is 24. 12.5% of 24 is 3.
    7. This sets the height of the placeholder to be the optimal size that fits the content without scrolling. Because the content only spans one line, the placeholder has its height set to 1.
    8. This sets the height to 1fr, which means this placeholder will have half the height of a placeholder with 2fr.
    9. This sets the height to 2fr, which means this placeholder will have twice the height of a placeholder with 1fr.
    "},{"location":"styles/height/#css","title":"CSS","text":"
    /* Explicit cell height */\nheight: 10;\n\n/* Percentage height */\nheight: 50%;\n\n/* Automatic height */\nheight: auto\n
    "},{"location":"styles/height/#python","title":"Python","text":"
    self.styles.height = 10  # Explicit cell height can be an int\nself.styles.height = \"50%\"\nself.styles.height = \"auto\"\n
    "},{"location":"styles/height/#see-also","title":"See also","text":"
    • max-height and min-height to limit the height of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/keyline/","title":"Keyline","text":"

    The keyline style is applied to a container and will draw lines around child widgets.

    A keyline is superficially like the border rule, but rather than draw inside the widget, a keyline is drawn outside of the widget's border. Additionally, unlike border, keylines can overlap and cross to create dividing lines between widgets.

    Because keylines are drawn in the widget's margin, you will need to apply the margin or grid-gutter rule to see the effect.

    "},{"location":"styles/keyline/#syntax","title":"Syntax","text":"
    \nkeyline: [<keyline>] [<color>];\n
    "},{"location":"styles/keyline/#examples","title":"Examples","text":""},{"location":"styles/keyline/#horizontal-keyline","title":"Horizontal Keyline","text":"

    The following examples shows a simple horizontal layout with a thin keyline.

    Outputkeyline.pykeyline.tcss

    KeylineApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Placeholder\u2502Placeholder\u2502Placeholder\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass KeylineApp(App):\n    CSS_PATH = \"keyline_horizontal.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Placeholder()\n            yield Placeholder()\n            yield Placeholder()\n\n\nif __name__ == \"__main__\":\n    app = KeylineApp()\n    app.run()\n
    Placeholder {\n    margin: 1;\n    width: 1fr;\n}\n\nHorizontal {\n    keyline: thin $secondary;\n}\n
    "},{"location":"styles/keyline/#grid-keyline","title":"Grid keyline","text":"

    The following examples shows a grid layout with a heavy keyline.

    Outputkeyline.pykeyline.tcss

    KeylineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503\u2503 \u2503\u2503\u2503 \u2503#foo\u2503\u2503 \u2503\u2503\u2503 \u2503\u2503\u2503 \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b#bar\u2503 \u2503\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2503Placeholder\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2503\u2503\u2503\u2503 \u2523\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u253b\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u252b \u2503\u2503 \u2503\u2503 \u2503#baz\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass KeylineApp(App):\n    CSS_PATH = \"keyline.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Grid():\n            yield Placeholder(id=\"foo\")\n            yield Placeholder(id=\"bar\")\n            yield Placeholder()\n            yield Placeholder(classes=\"hidden\")\n            yield Placeholder(id=\"baz\")\n\n\nif __name__ == \"__main__\":\n    KeylineApp().run()\n
    Grid {\n    grid-size: 3 3;\n    grid-gutter: 1;\n    padding: 2 3;\n    keyline: heavy green;\n}\nPlaceholder {\n    height: 1fr;      \n}\n.hidden {\n    visibility: hidden;\n} \n#foo {\n    column-span: 2;\n}\n#bar {\n    row-span: 2;        \n}\n#baz {\n    column-span:3;\n}\n
    "},{"location":"styles/keyline/#css","title":"CSS","text":"
    /* Set a thin green keyline */\n/* Note: Must be set on a container or a widget with a layout. */\nkeyline: thin green;\n
    "},{"location":"styles/keyline/#python","title":"Python","text":"

    You can set a keyline in Python with a tuple of type and color:

    widget.styles.keyline = (\"thin\", \"green\")\n
    "},{"location":"styles/keyline/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    "},{"location":"styles/layer/","title":"Layer","text":"

    The layer style defines the layer a widget belongs to.

    "},{"location":"styles/layer/#syntax","title":"Syntax","text":"
    \nlayer: <name>;\n

    The layer style accepts a <name> that defines the layer this widget belongs to. This <name> must correspond to a <name> that has been defined in a layers style by an ancestor of this widget.

    More information on layers can be found in the guide.

    Warning

    Using a <name> that hasn't been defined in a layers declaration of an ancestor of this widget has no effect.

    "},{"location":"styles/layer/#example","title":"Example","text":"

    In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"styles/layer/#css","title":"CSS","text":"
    /* Draw the widget on the layer called 'below' */\nlayer: below;\n
    "},{"location":"styles/layer/#python","title":"Python","text":"
    # Draw the widget on the layer called 'below'\nwidget.styles.layer = \"below\"\n
    "},{"location":"styles/layer/#see-also","title":"See also","text":"
    • The layout guide section on layers.
    • layers to define an ordered set of layers.
    "},{"location":"styles/layers/","title":"Layers","text":"

    The layers style allows you to define an ordered set of layers.

    "},{"location":"styles/layers/#syntax","title":"Syntax","text":"
    \nlayers: <name>+;\n

    The layers style accepts one or more <name> that define the layers that the widget is aware of, and the order in which they will be painted on the screen.

    The values used here can later be referenced using the layer property. The layers defined first in the list are drawn under the layers that are defined later in the list.

    More information on layers can be found in the guide.

    "},{"location":"styles/layers/#example","title":"Example","text":"

    In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

    Outputlayers.pylayers.tcss

    LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass LayersExample(App):\n    CSS_PATH = \"layers.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(\"box1 (layer = above)\", id=\"box1\")\n        yield Static(\"box2 (layer = below)\", id=\"box2\")\n\n\nif __name__ == \"__main__\":\n    app = LayersExample()\n    app.run()\n
    Screen {\n    align: center middle;\n    layers: below above;\n}\n\nStatic {\n    width: 28;\n    height: 8;\n    color: auto;\n    content-align: center middle;\n}\n\n#box1 {\n    layer: above;\n    background: darkcyan;\n}\n\n#box2 {\n    layer: below;\n    background: orange;\n    offset: 12 6;\n}\n
    "},{"location":"styles/layers/#css","title":"CSS","text":"
    /* Bottom layer is called 'below', layer above it is called 'above' */\nlayers: below above;\n
    "},{"location":"styles/layers/#python","title":"Python","text":"
    # Bottom layer is called 'below', layer above it is called 'above'\nwidget.style.layers = (\"below\", \"above\")\n
    "},{"location":"styles/layers/#see-also","title":"See also","text":"
    • The layout guide section on layers.
    • layer to set the layer a widget belongs to.
    "},{"location":"styles/layout/","title":"Layout","text":"

    The layout style defines how a widget arranges its children.

    "},{"location":"styles/layout/#syntax","title":"Syntax","text":"
    \nlayout: grid | horizontal | vertical;\n

    The layout style takes an option that defines how child widgets will be arranged, as per the table shown below.

    "},{"location":"styles/layout/#values","title":"Values","text":"Value Description grid Child widgets will be arranged in a grid. horizontal Child widgets will be arranged along the horizontal axis, from left to right. vertical (default) Child widgets will be arranged along the vertical axis, from top to bottom.

    See the layout guide for more information.

    "},{"location":"styles/layout/#example","title":"Example","text":"

    Note how the layout style affects the arrangement of widgets in the example below. To learn more about the grid layout, you can see the layout guide or the grid reference.

    Outputlayout.pylayout.tcss

    LayoutApp Layout Is Vertical LayoutIsHorizontal

    from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\n\n\nclass LayoutApp(App):\n    CSS_PATH = \"layout.tcss\"\n\n    def compose(self):\n        yield Container(\n            Label(\"Layout\"),\n            Label(\"Is\"),\n            Label(\"Vertical\"),\n            id=\"vertical-layout\",\n        )\n        yield Container(\n            Label(\"Layout\"),\n            Label(\"Is\"),\n            Label(\"Horizontal\"),\n            id=\"horizontal-layout\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = LayoutApp()\n    app.run()\n
    #vertical-layout {\n    layout: vertical;\n    background: darkmagenta;\n    height: auto;\n}\n\n#horizontal-layout {\n    layout: horizontal;\n    background: darkcyan;\n    height: auto;\n}\n\nLabel {\n    margin: 1;\n    width: 12;\n    color: black;\n    background: yellowgreen;\n}\n
    "},{"location":"styles/layout/#css","title":"CSS","text":"
    layout: horizontal;\n
    "},{"location":"styles/layout/#python","title":"Python","text":"
    widget.styles.layout = \"horizontal\"\n
    "},{"location":"styles/layout/#see-also","title":"See also","text":"
    • Layout guide.
    • Grid reference.
    "},{"location":"styles/margin/","title":"Margin","text":"

    The margin style specifies spacing around a widget.

    "},{"location":"styles/margin/#syntax","title":"Syntax","text":"
    \nmargin: <integer>\n      # one value for all edges\n      | <integer> <integer>\n      # top/bot   left/right\n      | <integer> <integer> <integer> <integer>;\n      # top       right     bot       left\n\nmargin-top: <integer>;\nmargin-right: <integer>;\nmargin-bottom: <integer>;\nmargin-left: <integer>;\n

    The margin specifies spacing around the four edges of the widget equal to the <integer> specified. The number of values given defines what edges get what margin:

    • 1 <integer> sets the same margin for the four edges of the widget;
    • 2 <integer> set margin for top/bottom and left/right edges, respectively.
    • 4 <integer> set margin for the top, right, bottom, and left edges, respectively.

    Tip

    To remember the order of the edges affected by the rule margin when it has 4 values, think of a clock. Its hand starts at the top and the goes clockwise: top, right, bottom, left.

    Alternatively, margin can be set for each edge individually through the styles margin-top, margin-right, margin-bottom, and margin-left, respectively.

    "},{"location":"styles/margin/#examples","title":"Examples","text":""},{"location":"styles/margin/#basic-usage","title":"Basic usage","text":"

    In the example below we add a large margin to a label, which makes it move away from the top-left corner of the screen.

    Outputmargin.pymargin.tcss

    MarginApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a \u258eits\u00a0path.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u258eremain.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass MarginApp(App):\n    CSS_PATH = \"margin.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = MarginApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n}\n\nLabel {\n    margin: 4 8;\n    background: blue 20%;\n    border: blue wide;\n    width: 100%;\n}\n
    "},{"location":"styles/margin/#all-margin-settings","title":"All margin settings","text":"

    The next example shows a grid. In each cell, we have a placeholder that has its margins set in different ways.

    Outputmargin_all.pymargin_all.tcss

    MarginAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin\u2502\u2502margin:\u00a01\u00a0\u2502 \u2502no\u00a0margin\u2502\u2502margin:\u00a01\u2502\u2502:\u00a01\u00a05\u2502\u25021\u00a02\u00a06\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin-bottom:\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502margin-right:\u00a0\u2502\u2502\u2502\u2502margin-left:\u00a03\u2502 \u2502\u2502\u25023\u2502\u2502\u2502\u2502\u2502 \u2502margin-top:\u00a04\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\n\n\nclass MarginAllApp(App):\n    CSS_PATH = \"margin_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Container(Placeholder(\"no margin\", id=\"p1\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1\", id=\"p2\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1 5\", id=\"p3\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin: 1 1 2 6\", id=\"p4\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-top: 4\", id=\"p5\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-right: 3\", id=\"p6\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-bottom: 4\", id=\"p7\"), classes=\"bordered\"),\n            Container(Placeholder(\"margin-left: 3\", id=\"p8\"), classes=\"bordered\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MarginAllApp()\n    app.run()\n
    Screen {\n    background: $background;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    width: 100%;\n    height: 100%;\n}\n\nContainer {\n    width: 100%;\n    height: 100%;\n}\n\n.bordered {\n    border: white round;\n}\n\n#p1 {\n    /* default is no margin */\n}\n\n#p2 {\n    margin: 1;\n}\n\n#p3 {\n    margin: 1 5;\n}\n\n#p4 {\n    margin: 1 1 2 6;\n}\n\n#p5 {\n    margin-top: 4;\n}\n\n#p6 {\n    margin-right: 3;\n}\n\n#p7 {\n    margin-bottom: 4;\n}\n\n#p8 {\n    margin-left: 3;\n}\n
    "},{"location":"styles/margin/#css","title":"CSS","text":"
    /* Set margin of 1 around all edges */\nmargin: 1;\n/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */\nmargin: 2 4;\n/* Set margin of 1 on the top, 2 on the right,\n                 3 on the bottom, and 4 on the left */\nmargin: 1 2 3 4;\n\nmargin-top: 1;\nmargin-right: 2;\nmargin-bottom: 3;\nmargin-left: 4;\n
    "},{"location":"styles/margin/#python","title":"Python","text":"

    Python does not provide the properties margin-top, margin-right, margin-bottom, and margin-left. However, you can set the margin to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

    # Set margin of 1 around all edges\nwidget.styles.margin = 1\n# Set margin of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.margin = (2, 4)\n# Set margin of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.margin = (1, 2, 3, 4)\n
    "},{"location":"styles/margin/#see-also","title":"See also","text":"
    • padding to add spacing around the content of a widget.
    "},{"location":"styles/max_height/","title":"Max-height","text":"

    The max-height style sets a maximum height for a widget.

    "},{"location":"styles/max_height/#syntax","title":"Syntax","text":"
    \nmax-height: <scalar>;\n

    The max-height style accepts a <scalar> that defines an upper bound for the height of a widget. That is, the height of a widget is never allowed to exceed max-height.

    "},{"location":"styles/max_height/#example","title":"Example","text":"

    The example below shows some placeholders that were defined to span vertically from the top edge of the terminal to the bottom edge. Then, we set max-height individually on each placeholder.

    Outputmax_height.pymax_height.tcss

    MaxHeightApp max-height:\u00a010w max-height:\u00a010 max-height:\u00a050% max-height:\u00a0999

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass MaxHeightApp(App):\n    CSS_PATH = \"max_height.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(\"max-height: 10w\", id=\"p1\"),\n            Placeholder(\"max-height: 999\", id=\"p2\"),\n            Placeholder(\"max-height: 50%\", id=\"p3\"),\n            Placeholder(\"max-height: 10\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MaxHeightApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n}\n\nPlaceholder {\n    height: 100%;\n    width: 1fr;\n}\n\n#p1 {\n    max-height: 10w;\n}\n\n#p2 {\n    max-height: 999;  /* (1)! */\n}\n\n#p3 {\n    max-height: 50%;\n}\n\n#p4 {\n    max-height: 10;\n}\n
    1. This won't affect the placeholder because its height is less than the maximum height.
    "},{"location":"styles/max_height/#css","title":"CSS","text":"
    /* Set the maximum height to 10 rows */\nmax-height: 10;\n\n/* Set the maximum height to 25% of the viewport height */\nmax-height: 25vh;\n
    "},{"location":"styles/max_height/#python","title":"Python","text":"
    # Set the maximum height to 10 rows\nwidget.styles.max_height = 10\n\n# Set the maximum height to 25% of the viewport height\nwidget.styles.max_height = \"25vh\"\n
    "},{"location":"styles/max_height/#see-also","title":"See also","text":"
    • min-height to set a lower bound on the height of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/max_width/","title":"Max-width","text":"

    The max-width style sets a maximum width for a widget.

    "},{"location":"styles/max_width/#syntax","title":"Syntax","text":"
    \nmax-width: <scalar>;\n

    The max-width style accepts a <scalar> that defines an upper bound for the width of a widget. That is, the width of a widget is never allowed to exceed max-width.

    "},{"location":"styles/max_width/#example","title":"Example","text":"

    The example below shows some placeholders that were defined to span horizontally from the left edge of the terminal to the right edge. Then, we set max-width individually on each placeholder.

    Outputmax_width.pymax_width.tcss

    MaxWidthApp max-width:\u00a0 50h max-width:\u00a0999 max-width:\u00a050% max-width:\u00a030

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass MaxWidthApp(App):\n    CSS_PATH = \"max_width.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(\"max-width: 50h\", id=\"p1\"),\n            Placeholder(\"max-width: 999\", id=\"p2\"),\n            Placeholder(\"max-width: 50%\", id=\"p3\"),\n            Placeholder(\"max-width: 30\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MaxWidthApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n}\n\nPlaceholder {\n    width: 100%;\n    height: 1fr;\n}\n\n#p1 {\n    max-width: 50h;\n}\n\n#p2 {\n    max-width: 999;  /* (1)! */\n}\n\n#p3 {\n    max-width: 50%;\n}\n\n#p4 {\n    max-width: 30;\n}\n
    1. This won't affect the placeholder because its width is less than the maximum width.
    "},{"location":"styles/max_width/#css","title":"CSS","text":"
    /* Set the maximum width to 10 rows */\nmax-width: 10;\n\n/* Set the maximum width to 25% of the viewport width */\nmax-width: 25vw;\n
    "},{"location":"styles/max_width/#python","title":"Python","text":"
    # Set the maximum width to 10 rows\nwidget.styles.max_width = 10\n\n# Set the maximum width to 25% of the viewport width\nwidget.styles.max_width = \"25vw\"\n
    "},{"location":"styles/max_width/#see-also","title":"See also","text":"
    • min-width to set a lower bound on the width of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/min_height/","title":"Min-height","text":"

    The min-height style sets a minimum height for a widget.

    "},{"location":"styles/min_height/#syntax","title":"Syntax","text":"
    \nmin-height: <scalar>;\n

    The min-height style accepts a <scalar> that defines a lower bound for the height of a widget. That is, the height of a widget is never allowed to be under min-height.

    "},{"location":"styles/min_height/#example","title":"Example","text":"

    The example below shows some placeholders with their height set to 50%. Then, we set min-height individually on each placeholder.

    Outputmin_height.pymin_height.tcss

    MinHeightApp min-height:\u00a025% min-height:\u00a075% min-height:\u00a030 min-height:\u00a040w \u2583\u2583

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\n\n\nclass MinHeightApp(App):\n    CSS_PATH = \"min_height.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(\"min-height: 25%\", id=\"p1\"),\n            Placeholder(\"min-height: 75%\", id=\"p2\"),\n            Placeholder(\"min-height: 30\", id=\"p3\"),\n            Placeholder(\"min-height: 40w\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MinHeightApp()\n    app.run()\n
    Horizontal {\n    height: 100%;\n    width: 100%;\n    overflow-y: auto;\n}\n\nPlaceholder {\n    width: 1fr;\n    height: 50%;\n}\n\n#p1 {\n    min-height: 25%;  /* (1)! */\n}\n\n#p2 {\n    min-height: 75%;\n}\n\n#p3 {\n    min-height: 30;\n}\n\n#p4 {\n    min-height: 40w;\n}\n
    1. This won't affect the placeholder because its height is larger than the minimum height.
    "},{"location":"styles/min_height/#css","title":"CSS","text":"
    /* Set the minimum height to 10 rows */\nmin-height: 10;\n\n/* Set the minimum height to 25% of the viewport height */\nmin-height: 25vh;\n
    "},{"location":"styles/min_height/#python","title":"Python","text":"
    # Set the minimum height to 10 rows\nwidget.styles.min_height = 10\n\n# Set the minimum height to 25% of the viewport height\nwidget.styles.min_height = \"25vh\"\n
    "},{"location":"styles/min_height/#see-also","title":"See also","text":"
    • max-height to set an upper bound on the height of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/min_width/","title":"Min-width","text":"

    The min-width style sets a minimum width for a widget.

    "},{"location":"styles/min_width/#syntax","title":"Syntax","text":"
    \nmin-width: <scalar>;\n

    The min-width style accepts a <scalar> that defines a lower bound for the width of a widget. That is, the width of a widget is never allowed to be under min-width.

    "},{"location":"styles/min_width/#example","title":"Example","text":"

    The example below shows some placeholders with their width set to 50%. Then, we set min-width individually on each placeholder.

    Outputmin_width.pymin_width.tcss

    MinWidthApp min-width:\u00a025% min-width:\u00a075% min-width:\u00a0100 min-width:\u00a0400h

    from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass MinWidthApp(App):\n    CSS_PATH = \"min_width.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Placeholder(\"min-width: 25%\", id=\"p1\"),\n            Placeholder(\"min-width: 75%\", id=\"p2\"),\n            Placeholder(\"min-width: 100\", id=\"p3\"),\n            Placeholder(\"min-width: 400h\", id=\"p4\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MinWidthApp()\n    app.run()\n
    VerticalScroll {\n    height: 100%;\n    width: 100%;\n    overflow-x: auto;\n}\n\nPlaceholder {\n    height: 1fr;\n    width: 50%;\n}\n\n#p1 {\n    min-width: 25%;\n    /* (1)! */\n}\n\n#p2 {\n    min-width: 75%;\n}\n\n#p3 {\n    min-width: 100;\n}\n\n#p4 {\n    min-width: 400h;\n}\n
    1. This won't affect the placeholder because its width is larger than the minimum width.
    "},{"location":"styles/min_width/#css","title":"CSS","text":"
    /* Set the minimum width to 10 rows */\nmin-width: 10;\n\n/* Set the minimum width to 25% of the viewport width */\nmin-width: 25vw;\n
    "},{"location":"styles/min_width/#python","title":"Python","text":"
    # Set the minimum width to 10 rows\nwidget.styles.min_width = 10\n\n# Set the minimum width to 25% of the viewport width\nwidget.styles.min_width = \"25vw\"\n
    "},{"location":"styles/min_width/#see-also","title":"See also","text":"
    • max-width to set an upper bound on the width of a widget.
    • width to set the width of a widget.
    "},{"location":"styles/offset/","title":"Offset","text":"

    The offset style defines an offset for the position of the widget.

    "},{"location":"styles/offset/#syntax","title":"Syntax","text":"
    \noffset: <scalar> <scalar>;\n\noffset-x: <scalar>;\noffset-y: <scalar>\n

    The two <scalar> in the offset define, respectively, the offsets in the horizontal and vertical axes for the widget.

    To specify an offset along a single axis, you can use offset-x and offset-y.

    "},{"location":"styles/offset/#example","title":"Example","text":"

    In this example, we have 3 widgets with differing offsets.

    Outputoffset.pyoffset.tcss

    OffsetApp \u258c\u2590 \u258cChani\u00a0(offset\u00a00\u00a0\u2590 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u258c-3)\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258cPaul\u00a0(offset\u00a08\u00a02)\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u258c\u2590 \u258c\u2590 \u258c\u2590 \u258c\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u258c\u2590 \u258c\u2590 \u258c\u2590 \u258cDuncan\u00a0(offset\u00a04\u00a0\u2590 \u258c10)\u2590 \u258c\u2590 \u258c\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass OffsetApp(App):\n    CSS_PATH = \"offset.tcss\"\n\n    def compose(self):\n        yield Label(\"Paul (offset 8 2)\", classes=\"paul\")\n        yield Label(\"Duncan (offset 4 10)\", classes=\"duncan\")\n        yield Label(\"Chani (offset 0 -3)\", classes=\"chani\")\n\n\nif __name__ == \"__main__\":\n    app = OffsetApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n    layout: horizontal;\n}\nLabel {\n    width: 20;\n    height: 10;\n    content-align: center middle;\n}\n\n.paul {\n    offset: 8 2;\n    background: red 20%;\n    border: outer red;\n    color: red;\n}\n\n.duncan {\n    offset: 4 10;\n    background: green 20%;\n    border: outer green;\n    color: green;\n}\n\n.chani {\n    offset: 0 -3;\n    background: blue 20%;\n    border: outer blue;\n    color: blue;\n}\n
    "},{"location":"styles/offset/#css","title":"CSS","text":"
    /* Move the widget 8 cells in the x direction and 2 in the y direction */\noffset: 8 2;\n\n/* Move the widget 4 cells in the x direction\noffset-x: 4;\n/* Move the widget -3 cells in the y direction\noffset-y: -3;\n
    "},{"location":"styles/offset/#python","title":"Python","text":"

    You cannot change programmatically the offset for a single axis. You have to set the two axes at the same time.

    # Move the widget 2 cells in the x direction, and 4 in the y direction.\nwidget.styles.offset = (2, 4)\n
    "},{"location":"styles/offset/#see-also","title":"See also","text":"
    • The layout guide section on offsets.
    "},{"location":"styles/opacity/","title":"Opacity","text":"

    The opacity style sets the opacity of a widget.

    While terminals are not capable of true opacity, Textual can create an approximation by blending widgets with their background color.

    "},{"location":"styles/opacity/#syntax","title":"Syntax","text":"
    \nopacity: <number> | <percentage>;\n

    The opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then opacity should be a value between 0 and 1, where 0 is the background color and 1 is fully opaque. If given as a percentage, 0% is the background color and 100% is fully opaque.

    Typically, if you set this value it would be somewhere between the two extremes. For instance, setting the opacity of a widget to 70% will make it appear dimmer than surrounding widgets, which could be used to display a disabled state.

    "},{"location":"styles/opacity/#example","title":"Example","text":"

    This example shows, from top to bottom, increasing opacity values for a label with a border and some text. When the opacity is zero, all we see is the (black) background.

    Outputopacity.pyopacity.tcss

    OpacityApp \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258copacity:\u00a00%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a025%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a050%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a075%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a0100%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass OpacityApp(App):\n    CSS_PATH = \"opacity.tcss\"\n\n    def compose(self):\n        yield Label(\"opacity: 0%\", id=\"zero-opacity\")\n        yield Label(\"opacity: 25%\", id=\"quarter-opacity\")\n        yield Label(\"opacity: 50%\", id=\"half-opacity\")\n        yield Label(\"opacity: 75%\", id=\"three-quarter-opacity\")\n        yield Label(\"opacity: 100%\", id=\"full-opacity\")\n\n\nif __name__ == \"__main__\":\n    app = OpacityApp()\n    app.run()\n
    #zero-opacity {\n    opacity: 0%;\n}\n\n#quarter-opacity {\n    opacity: 25%;\n}\n\n#half-opacity {\n    opacity: 50%;\n}\n\n#three-quarter-opacity {\n    opacity: 75%;\n}\n\n#full-opacity {\n    opacity: 100%;\n}\n\nScreen {\n    background: black;\n}\n\nLabel {\n    width: 100%;\n    height: 1fr;\n    border: outer dodgerblue;\n    background: lightseagreen;\n    content-align: center middle;\n    text-style: bold;\n}\n
    "},{"location":"styles/opacity/#css","title":"CSS","text":"
    /* Fade the widget to 50% against its parent's background */\nopacity: 50%;\n
    "},{"location":"styles/opacity/#python","title":"Python","text":"
    # Fade the widget to 50% against its parent's background\nwidget.styles.opacity = \"50%\"\n
    "},{"location":"styles/opacity/#see-also","title":"See also","text":"
    • text-opacity to blend the color of a widget's content with its background color.
    "},{"location":"styles/outline/","title":"Outline","text":"

    The outline style enables the drawing of a box around the content of a widget, which means the outline is drawn over the content area.

    Note

    border and outline cannot coexist in the same edge of a widget.

    "},{"location":"styles/outline/#syntax","title":"Syntax","text":"
    \noutline: [<border>] [<color>];\n\noutline-top: [<border>] [<color>];\noutline-right: [<border>] [<color>];\noutline-bottom: [<border>] [<color>];\noutline-left: [<border>] [<color>];\n

    The style outline accepts an optional <border> that sets the visual style of the widget outline and an optional <color> to set the color of the outline.

    Unlike the style border, the frame of the outline is drawn over the content area of the widget. This rule can be useful to add temporary emphasis on the content of a widget, if you want to draw the user's attention to it.

    "},{"location":"styles/outline/#border-command","title":"Border command","text":"

    The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

    textual borders\n
    "},{"location":"styles/outline/#examples","title":"Examples","text":""},{"location":"styles/outline/#basic-usage","title":"Basic usage","text":"

    This example shows a widget with an outline. Note how the outline occludes the text area.

    Outputoutline.pyoutline.tcss

    OutlineApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258e\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258e\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258end\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u258a \u258eath.\u258a \u258ehere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineApp(App):\n    CSS_PATH = \"outline.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = OutlineApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: black;\n}\n\nLabel {\n    margin: 4 8;\n    background: green 20%;\n    outline: wide green;\n    width: 100%;\n}\n
    "},{"location":"styles/outline/#all-outline-types","title":"All outline types","text":"

    The next example shows a grid with all the available outline types.

    Outputoutline_all.pyoutline_all.tcss

    AllOutlinesApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596 hkey\u2590inner\u258cnone \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u258couter\u2590\u2502round\u2502\u2502solid\u2502 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258atall\u258e\u258fvkey\u2595\u258ewide\u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass AllOutlinesApp(App):\n    CSS_PATH = \"outline_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"ascii\", id=\"ascii\"),\n            Label(\"blank\", id=\"blank\"),\n            Label(\"dashed\", id=\"dashed\"),\n            Label(\"double\", id=\"double\"),\n            Label(\"heavy\", id=\"heavy\"),\n            Label(\"hidden/none\", id=\"hidden\"),\n            Label(\"hkey\", id=\"hkey\"),\n            Label(\"inner\", id=\"inner\"),\n            Label(\"none\", id=\"none\"),\n            Label(\"outer\", id=\"outer\"),\n            Label(\"round\", id=\"round\"),\n            Label(\"solid\", id=\"solid\"),\n            Label(\"tall\", id=\"tall\"),\n            Label(\"vkey\", id=\"vkey\"),\n            Label(\"wide\", id=\"wide\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllOutlinesApp()\n    app.run()\n
    #ascii {\n    outline: ascii $accent;\n}\n\n#blank {\n    outline: blank $accent;\n}\n\n#dashed {\n    outline: dashed $accent;\n}\n\n#double {\n    outline: double $accent;\n}\n\n#heavy {\n    outline: heavy $accent;\n}\n\n#hidden {\n    outline: hidden $accent;\n}\n\n#hkey {\n    outline: hkey $accent;\n}\n\n#inner {\n    outline: inner $accent;\n}\n\n#none {\n    outline: none $accent;\n}\n\n#outer {\n    outline: outer $accent;\n}\n\n#round {\n    outline: round $accent;\n}\n\n#solid {\n    outline: solid $accent;\n}\n\n#tall {\n    outline: tall $accent;\n}\n\n#vkey {\n    outline: vkey $accent;\n}\n\n#wide {\n    outline: wide $accent;\n}\n\nGrid {\n    grid-size: 3 5;\n    align: center middle;\n    grid-gutter: 1 2;\n}\n\nLabel {\n    width: 20;\n    height: 3;\n    content-align: center middle;\n}\n
    "},{"location":"styles/outline/#borders-and-outlines","title":"Borders and outlines","text":"

    The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

    This example also shows that a widget cannot contain both a border and an outline:

    Outputoutline_vs_border.pyoutline_vs_border.tcss

    OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OutlineBorderApp(App):\n    CSS_PATH = \"outline_vs_border.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, classes=\"outline\")\n        yield Label(TEXT, classes=\"border\")\n        yield Label(TEXT, classes=\"outline border\")\n\n\nif __name__ == \"__main__\":\n    app = OutlineBorderApp()\n    app.run()\n
    Label {\n    height: 8;\n}\n\n.outline {\n    outline: $error round;\n}\n\n.border {\n    border: $success heavy;\n}\n
    "},{"location":"styles/outline/#css","title":"CSS","text":"
    /* Set a heavy white outline */\noutline:heavy white;\n\n/* set a red outline on the left */\noutline-left:outer red;\n
    "},{"location":"styles/outline/#python","title":"Python","text":"
    # Set a heavy white outline\nwidget.outline = (\"heavy\", \"white\")\n\n# Set a red outline on the left\nwidget.outline_left = (\"outer\", \"red\")\n
    "},{"location":"styles/outline/#see-also","title":"See also","text":"
    • border to add a border around a widget.
    "},{"location":"styles/overflow/","title":"Overflow","text":"

    The overflow style specifies if and when scrollbars should be displayed.

    "},{"location":"styles/overflow/#syntax","title":"Syntax","text":"
    \noverflow: <overflow> <overflow>;\n\noverflow-x: <overflow>;\noverflow-y: <overflow>;\n

    The style overflow accepts two values that determine when to display scrollbars in a container widget. The two values set the overflow for the horizontal and vertical axes, respectively.

    Overflow may also be set individually for each axis:

    • overflow-x sets the overflow for the horizontal axis; and
    • overflow-y sets the overflow for the vertical axis.
    "},{"location":"styles/overflow/#defaults","title":"Defaults","text":"

    The default setting for containers is overflow: auto auto.

    Warning

    Some built-in containers like Horizontal and VerticalScroll override these defaults.

    "},{"location":"styles/overflow/#example","title":"Example","text":"

    Here we split the screen into left and right sections, each with three vertically scrolling widgets that do not fit into the height of the terminal.

    The left side has overflow-y: auto (the default) and will automatically show a scrollbar. The right side has overflow-y: hidden which will prevent a scrollbar from being shown.

    Outputoverflow.pyoverflow.tcss

    OverflowApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eand\u00a0through\u00a0me.\u258a\u258eand\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0\u258a\u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0\u258a \u258ewill\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a\u258eturn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u258a \u258eits\u00a0path.\u258a\u2581\u2581\u258epath.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0\u258a\u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u258a \u258ewill\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a\u258ebe\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u258a \u258eremain.\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eand\u00a0through\u00a0me.\u258a

    from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass OverflowApp(App):\n    CSS_PATH = \"overflow.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"left\"),\n            VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"right\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = OverflowApp()\n    app.run()\n
    Screen {\n    background: $background;\n    color: black;\n}\n\nVerticalScroll {\n    width: 1fr;\n}\n\nStatic {\n    margin: 1 2;\n    background: green 80%;\n    border: green wide;\n    color: white 90%;\n    height: auto;\n}\n\n#right {\n    overflow-y: hidden;\n}\n
    "},{"location":"styles/overflow/#css","title":"CSS","text":"
    /* Automatic scrollbars on both axes (the default) */\noverflow: auto auto;\n\n/* Hide the vertical scrollbar */\noverflow-y: hidden;\n\n/* Always show the horizontal scrollbar */\noverflow-x: scroll;\n
    "},{"location":"styles/overflow/#python","title":"Python","text":"

    Overflow cannot be programmatically set for both axes at the same time.

    # Hide the vertical scrollbar\nwidget.styles.overflow_y = \"hidden\"\n\n# Always show the horizontal scrollbar\nwidget.styles.overflow_x = \"scroll\"\n
    "},{"location":"styles/padding/","title":"Padding","text":"

    The padding style specifies spacing around the content of a widget.

    "},{"location":"styles/padding/#syntax","title":"Syntax","text":"
    \npadding: <integer> # one value for all edges\n       | <integer> <integer>\n       # top/bot   left/right\n       | <integer> <integer> <integer> <integer>;\n       # top       right     bot       left\n\npadding-top: <integer>;\npadding-right: <integer>;\npadding-bottom: <integer>;\npadding-left: <integer>;\n

    The padding specifies spacing around the content of a widget, thus this spacing is added inside the widget. The values of the <integer> determine how much spacing is added and the number of values define what edges get what padding:

    • 1 <integer> sets the same padding for the four edges of the widget;
    • 2 <integer> set padding for top/bottom and left/right edges, respectively.
    • 4 <integer> set padding for the top, right, bottom, and left edges, respectively.

    Tip

    To remember the order of the edges affected by the rule padding when it has 4 values, think of a clock. Its hand starts at the top and then goes clockwise: top, right, bottom, left.

    Alternatively, padding can be set for each edge individually through the rules padding-top, padding-right, padding-bottom, and padding-left, respectively.

    "},{"location":"styles/padding/#example","title":"Example","text":""},{"location":"styles/padding/#basic-usage","title":"Basic usage","text":"

    This example adds padding around some text.

    Outputpadding.pypadding.tcss

    PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass PaddingApp(App):\n    CSS_PATH = \"padding.tcss\"\n\n    def compose(self):\n        yield Label(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = PaddingApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: blue;\n}\n\nLabel {\n    padding: 4 8;\n    background: blue 20%;\n    width: 100%;\n}\n
    "},{"location":"styles/padding/#all-padding-settings","title":"All padding settings","text":"

    The next example shows a grid. In each cell, we have a placeholder that has its padding set in different ways. The effect of each padding setting is noticeable in the colored background around the text of each placeholder.

    Outputpadding_all.pypadding_all.tcss

    PaddingAllApp no\u00a0padding padding:\u00a01padding:padding:\u00a01\u00a01 1\u00a052\u00a06 padding-right:\u00a03padding-bottom:\u00a04padding-left:\u00a03 padding-top:\u00a04

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass PaddingAllApp(App):\n    CSS_PATH = \"padding_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(\"no padding\", id=\"p1\"),\n            Placeholder(\"padding: 1\", id=\"p2\"),\n            Placeholder(\"padding: 1 5\", id=\"p3\"),\n            Placeholder(\"padding: 1 1 2 6\", id=\"p4\"),\n            Placeholder(\"padding-top: 4\", id=\"p5\"),\n            Placeholder(\"padding-right: 3\", id=\"p6\"),\n            Placeholder(\"padding-bottom: 4\", id=\"p7\"),\n            Placeholder(\"padding-left: 3\", id=\"p8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = PaddingAllApp()\n    app.run()\n
    Screen {\n    background: $background;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    width: auto;\n    height: auto;\n}\n\n#p1 {\n    /* default is no padding */\n}\n\n#p2 {\n    padding: 1;\n}\n\n#p3 {\n    padding: 1 5;\n}\n\n#p4 {\n    padding: 1 1 2 6;\n}\n\n#p5 {\n    padding-top: 4;\n}\n\n#p6 {\n    padding-right: 3;\n}\n\n#p7 {\n    padding-bottom: 4;\n}\n\n#p8 {\n    padding-left: 3;\n}\n
    "},{"location":"styles/padding/#css","title":"CSS","text":"
    /* Set padding of 1 around all edges */\npadding: 1;\n/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */\npadding: 2 4;\n/* Set padding of 1 on the top, 2 on the right,\n                 3 on the bottom, and 4 on the left */\npadding: 1 2 3 4;\n\npadding-top: 1;\npadding-right: 2;\npadding-bottom: 3;\npadding-left: 4;\n
    "},{"location":"styles/padding/#python","title":"Python","text":"

    In Python, you cannot set any of the individual padding styles padding-top, padding-right, padding-bottom, and padding-left.

    However, you can set padding to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

    # Set padding of 1 around all edges\nwidget.styles.padding = 1\n# Set padding of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.padding = (2, 4)\n# Set padding of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.padding = (1, 2, 3, 4)\n
    "},{"location":"styles/padding/#see-also","title":"See also","text":"
    • box-sizing to specify how to account for padding in a widget's dimensions.
    • padding to add spacing around a widget.
    "},{"location":"styles/scrollbar_gutter/","title":"Scrollbar-gutter","text":"

    The scrollbar-gutter style allows reserving space for a vertical scrollbar.

    "},{"location":"styles/scrollbar_gutter/#syntax","title":"Syntax","text":"
    \nscrollbar-gutter: auto | stable;\n
    "},{"location":"styles/scrollbar_gutter/#values","title":"Values","text":"Value Description auto (default) No space is reserved for a vertical scrollbar. stable Space is reserved for a vertical scrollbar.

    Setting the value to stable prevents unwanted layout changes when the scrollbar becomes visible, whereas the default value of auto means that the layout of your application is recomputed when a vertical scrollbar becomes needed.

    "},{"location":"styles/scrollbar_gutter/#example","title":"Example","text":"

    In the example below, notice the gap reserved for the scrollbar on the right side of the terminal window.

    Outputscrollbar_gutter.pyscrollbar_gutter.tcss

    ScrollbarGutterApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    from textual.app import App\nfrom textual.widgets import Static\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass ScrollbarGutterApp(App):\n    CSS_PATH = \"scrollbar_gutter.tcss\"\n\n    def compose(self):\n        yield Static(TEXT, id=\"text-box\")\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarGutterApp()\n    app.run()\n
    Screen {\n    scrollbar-gutter: stable;\n}\n\n#text-box {\n    color: floralwhite;\n    background: darkmagenta;\n}\n
    "},{"location":"styles/scrollbar_gutter/#css","title":"CSS","text":"
    scrollbar-gutter: auto;    /* Don't reserve space for a vertical scrollbar. */\nscrollbar-gutter: stable;  /* Reserve space for a vertical scrollbar. */\n
    "},{"location":"styles/scrollbar_gutter/#python","title":"Python","text":"
    self.styles.scrollbar_gutter = \"auto\"    # Don't reserve space for a vertical scrollbar.\nself.styles.scrollbar_gutter = \"stable\"  # Reserve space for a vertical scrollbar.\n
    "},{"location":"styles/scrollbar_size/","title":"Scrollbar-size","text":"

    The scrollbar-size style defines the width of the scrollbars.

    "},{"location":"styles/scrollbar_size/#syntax","title":"Syntax","text":"
    \nscrollbar-size: <integer> <integer>;\n              # horizontal vertical\n\nscrollbar-size-horizontal: <integer>;\nscrollbar-size-vertical: <integer>;\n

    The scrollbar-size style takes two <integer> to set the horizontal and vertical scrollbar sizes, respectively. This customisable size is the width of the scrollbar, given that its length will always be 100% of the container.

    The scrollbar widths may also be set individually with scrollbar-size-horizontal and scrollbar-size-vertical.

    "},{"location":"styles/scrollbar_size/#examples","title":"Examples","text":""},{"location":"styles/scrollbar_size/#basic-usage","title":"Basic usage","text":"

    In this example we modify the size of the widget's scrollbar to be much larger than usual.

    Outputscrollbar_size.pyscrollbar_size.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2581\u2581\u2581\u2581 I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    from textual.app import App\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbar_size.tcss\"\n\n    def compose(self):\n        yield ScrollableContainer(Label(TEXT * 5), classes=\"panel\")\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    Screen {\n    background: white;\n    color: blue 80%;\n    layout: horizontal;\n}\n\nLabel {\n    padding: 1 2;\n    width: 200;\n}\n\n.panel {\n    scrollbar-size: 10 4;\n    padding: 1 2;\n}\n
    "},{"location":"styles/scrollbar_size/#scrollbar-sizes-comparison","title":"Scrollbar sizes comparison","text":"

    In the next example we show three containers with differently sized scrollbars.

    Tip

    If you want to hide the scrollbar but still allow the container to scroll using the mousewheel or keyboard, you can set the scrollbar size to 0.

    Outputscrollbar_size2.pyscrollbar_size2.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587\u2587 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.\u2582I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 \u258fAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u258f \u258fWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0th\u258f \u258fI\u00a0must\u00a0not\u00a0fear.\u258f \u258fFear\u00a0is\u00a0the\u00a0mind-killer.\u258f \u258f\u2589\u258f

    from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbar_size2.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            ScrollableContainer(Label(TEXT * 5), id=\"v1\"),\n            ScrollableContainer(Label(TEXT * 5), id=\"v2\"),\n            ScrollableContainer(Label(TEXT * 5), id=\"v3\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    ScrollableContainer {\n    width: 1fr;\n}\n\n#v1 {\n    scrollbar-size: 5 1;\n    background: red 20%;\n}\n\n#v2 {\n    scrollbar-size-vertical: 1;\n    background: green 20%;\n}\n\n#v3 {\n    scrollbar-size-horizontal: 5;\n    background: blue 20%;\n}\n
    "},{"location":"styles/scrollbar_size/#css","title":"CSS","text":"
    /* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */\nscrollbar-size: 10 4;\n\n/* Set horizontal scrollbar to 10 */\nscrollbar-size-horizontal: 10;\n\n/* Set vertical scrollbar to 4 */\nscrollbar-size-vertical: 4;\n
    "},{"location":"styles/scrollbar_size/#python","title":"Python","text":"

    The style scrollbar-size has no Python equivalent. The scrollbar sizes must be set independently:

    # Set horizontal scrollbar to 10:\nwidget.styles.scrollbar_size_horizontal = 10\n# Set vertical scrollbar to 4:\nwidget.styles.scrollbar_size_vertical = 4\n
    "},{"location":"styles/text_align/","title":"Text-align","text":"

    The text-align style sets the text alignment in a widget.

    "},{"location":"styles/text_align/#syntax","title":"Syntax","text":"
    \ntext-align: <text-align>;\n

    The text-align style accepts a value of the type <text-align> that defines how text is aligned inside the widget.

    "},{"location":"styles/text_align/#defaults","title":"Defaults","text":"

    The default value is start.

    "},{"location":"styles/text_align/#example","title":"Example","text":"

    This example shows, from top to bottom: left, center, right, and justify text alignments.

    Outputtext_align.pytext_align.tcss

    TextAlign Left\u00a0alignedCenter\u00a0aligned I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0 mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0 obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Right\u00a0alignedJustified \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0theI\u00a0\u00a0must\u00a0\u00a0not\u00a0\u00a0fear.\u00a0\u00a0Fear\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0themind-killer.\u00a0\u00a0\u00a0\u00a0\u00a0Fear\u00a0\u00a0\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0totallittle-death\u00a0\u00a0\u00a0that\u00a0\u00a0\u00a0brings\u00a0\u00a0\u00a0total obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I \u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0andwill\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u00a0me\u00a0\u00a0and \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.through\u00a0me.

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\nTEXT = (\n    \"I must not fear. Fear is the mind-killer. Fear is the little-death that \"\n    \"brings total obliteration. I will face my fear. I will permit it to pass over \"\n    \"me and through me.\"\n)\n\n\nclass TextAlign(App):\n    CSS_PATH = \"text_align.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"[b]Left aligned[/]\\n\" + TEXT, id=\"one\"),\n            Label(\"[b]Center aligned[/]\\n\" + TEXT, id=\"two\"),\n            Label(\"[b]Right aligned[/]\\n\" + TEXT, id=\"three\"),\n            Label(\"[b]Justified[/]\\n\" + TEXT, id=\"four\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = TextAlign()\n    app.run()\n
    #one {\n    text-align: left;\n    background: lightblue;\n}\n\n#two {\n    text-align: center;\n    background: indianred;\n}\n\n#three {\n    text-align: right;\n    background: palegreen;\n}\n\n#four {\n    text-align: justify;\n    background: palevioletred;\n}\n\nLabel {\n    padding: 1 2;\n    height: 100%;\n    color: auto;\n}\n\nGrid {\n    grid-size: 2 2;\n}\n
    "},{"location":"styles/text_align/#css","title":"CSS","text":"
    /* Set text in the widget to be right aligned */\ntext-align: right;\n
    "},{"location":"styles/text_align/#python","title":"Python","text":"
    # Set text in the widget to be right aligned\nwidget.styles.text_align = \"right\"\n
    "},{"location":"styles/text_align/#see-also","title":"See also","text":"
    • align to set the alignment of children widgets inside a container.
    • content-align to set the alignment of content inside a widget.
    "},{"location":"styles/text_opacity/","title":"Text-opacity","text":"

    The text-opacity style blends the foreground color (i.e. text) with the background color.

    "},{"location":"styles/text_opacity/#syntax","title":"Syntax","text":"
    \ntext-opacity: <number> | <percentage>;\n

    The text opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then text-opacity should be a value between 0 and 1, where 0 makes the foreground color match the background (effectively making text invisible) and 1 will display text as normal. If given as a percentage, 0% will result in invisible text, and 100% will display fully opaque text.

    Typically, if you set this value it would be somewhere between the two extremes. For instance, setting text-opacity to 70% would result in slightly faded text. Setting it to 0.3 would result in very dim text.

    Warning

    Be careful not to set text opacity so low as to make it hard to read.

    "},{"location":"styles/text_opacity/#example","title":"Example","text":"

    This example shows, from top to bottom, increasing text-opacity values.

    Outputtext_opacity.pytext_opacity.tcss

    TextOpacityApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a025%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a050%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a075%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a0100%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass TextOpacityApp(App):\n    CSS_PATH = \"text_opacity.tcss\"\n\n    def compose(self):\n        yield Label(\"text-opacity: 0%\", id=\"zero-opacity\")\n        yield Label(\"text-opacity: 25%\", id=\"quarter-opacity\")\n        yield Label(\"text-opacity: 50%\", id=\"half-opacity\")\n        yield Label(\"text-opacity: 75%\", id=\"three-quarter-opacity\")\n        yield Label(\"text-opacity: 100%\", id=\"full-opacity\")\n\n\nif __name__ == \"__main__\":\n    app = TextOpacityApp()\n    app.run()\n
    #zero-opacity {\n    text-opacity: 0%;\n}\n\n#quarter-opacity {\n    text-opacity: 25%;\n}\n\n#half-opacity {\n    text-opacity: 50%;\n}\n\n#three-quarter-opacity {\n    text-opacity: 75%;\n}\n\n#full-opacity {\n    text-opacity: 100%;\n}\n\nLabel {\n    height: 1fr;\n    width: 100%;\n    text-align: center;\n    text-style: bold;\n}\n
    "},{"location":"styles/text_opacity/#css","title":"CSS","text":"
    /* Set the text to be \"half-faded\" against the background of the widget */\ntext-opacity: 50%;\n
    "},{"location":"styles/text_opacity/#python","title":"Python","text":"
    # Set the text to be \"half-faded\" against the background of the widget\nwidget.styles.text_opacity = \"50%\"\n
    "},{"location":"styles/text_opacity/#see-also","title":"See also","text":"
    • opacity to specify the opacity of a whole widget.
    "},{"location":"styles/text_style/","title":"Text-style","text":"

    The text-style style sets the style for the text in a widget.

    "},{"location":"styles/text_style/#syntax","title":"Syntax","text":"
    \ntext-style: <text-style>;\n

    text-style will take all the values specified and will apply that styling combination to the text in the widget.

    "},{"location":"styles/text_style/#examples","title":"Examples","text":""},{"location":"styles/text_style/#basic-usage","title":"Basic usage","text":"

    Each of the three text panels has a different text style, respectively bold, italic, and reverse, from left to right.

    Outputtext_style.pytext_style.tcss

    TextStyleApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0 I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Onlythere\u00a0will\u00a0be\u00a0nothing.\u00a0Only Only\u00a0I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass TextStyleApp(App):\n    CSS_PATH = \"text_style.tcss\"\n\n    def compose(self):\n        yield Label(TEXT, id=\"lbl1\")\n        yield Label(TEXT, id=\"lbl2\")\n        yield Label(TEXT, id=\"lbl3\")\n\n\nif __name__ == \"__main__\":\n    app = TextStyleApp()\n    app.run()\n
    Screen {\n    layout: horizontal;\n}\nLabel {\n    width: 1fr;\n}\n#lbl1 {\n    background: red 30%;\n    text-style: bold;\n}\n#lbl2 {\n    background: green 30%;\n    text-style: italic;\n}\n#lbl3 {\n    background: blue 30%;\n    text-style: reverse;\n}\n
    "},{"location":"styles/text_style/#all-text-styles","title":"All text styles","text":"

    The next example shows all different text styles on their own, as well as some combinations of styles in a single widget.

    Outputtext_style_all.pytext_style_all.tcss

    AllTextStyleApp nonebolditalicreverse I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. strikeunderlinebold\u00a0italicreverse\u00a0strike I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass AllTextStyleApp(App):\n    CSS_PATH = \"text_style_all.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"none\\n\" + TEXT, id=\"lbl1\"),\n            Label(\"bold\\n\" + TEXT, id=\"lbl2\"),\n            Label(\"italic\\n\" + TEXT, id=\"lbl3\"),\n            Label(\"reverse\\n\" + TEXT, id=\"lbl4\"),\n            Label(\"strike\\n\" + TEXT, id=\"lbl5\"),\n            Label(\"underline\\n\" + TEXT, id=\"lbl6\"),\n            Label(\"bold italic\\n\" + TEXT, id=\"lbl7\"),\n            Label(\"reverse strike\\n\" + TEXT, id=\"lbl8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = AllTextStyleApp()\n    app.run()\n
    #lbl1 {\n    text-style: none;\n}\n\n#lbl2 {\n    text-style: bold;\n}\n\n#lbl3 {\n    text-style: italic;\n}\n\n#lbl4 {\n    text-style: reverse;\n}\n\n#lbl5 {\n    text-style: strike;\n}\n\n#lbl6 {\n    text-style: underline;\n}\n\n#lbl7 {\n    text-style: bold italic;\n}\n\n#lbl8 {\n    text-style: reverse strike;\n}\n\nGrid {\n    grid-size: 4;\n    grid-gutter: 1 2;\n    margin: 1 2;\n    height: 100%;\n}\n\nLabel {\n    height: 100%;\n}\n
    "},{"location":"styles/text_style/#css","title":"CSS","text":"
    text-style: italic;\n
    "},{"location":"styles/text_style/#python","title":"Python","text":"
    widget.styles.text_style = \"italic\"\n
    "},{"location":"styles/tint/","title":"Tint","text":"

    The tint style blends a color with the whole widget.

    "},{"location":"styles/tint/#syntax","title":"Syntax","text":"
    \ntint: <color> [<percentage>];\n

    The tint style blends a <color> with the widget. The color should likely have an alpha component (specified directly in the color used or by the optional <percentage>), otherwise the end result will obscure the widget content.

    "},{"location":"styles/tint/#example","title":"Example","text":"

    This examples shows a green tint with gradually increasing alpha.

    Outputtint.pytint.tcss

    TintApp tint:\u00a0green\u00a00%; tint:\u00a0green\u00a010%; tint:\u00a0green\u00a020%; tint:\u00a0green\u00a030%; tint:\u00a0green\u00a040%; tint:\u00a0green\u00a050%; \u2584\u2584 tint:\u00a0green\u00a060%; tint:\u00a0green\u00a070%;

    from textual.app import App\nfrom textual.color import Color\nfrom textual.widgets import Label\n\n\nclass TintApp(App):\n    CSS_PATH = \"tint.tcss\"\n\n    def compose(self):\n        color = Color.parse(\"green\")\n        for tint_alpha in range(0, 101, 10):\n            widget = Label(f\"tint: green {tint_alpha}%;\")\n            widget.styles.tint = color.with_alpha(tint_alpha / 100)  # (1)!\n            yield widget\n\n\nif __name__ == \"__main__\":\n    app = TintApp()\n    app.run()\n
    1. We set the tint to a Color instance with varying levels of opacity, set through the method with_alpha.
    Label {\n    height: 3;\n    width: 100%;\n    text-style: bold;\n    background: white;\n    color: black;\n    content-align: center middle;\n}\n
    "},{"location":"styles/tint/#css","title":"CSS","text":"
    /* A red tint (could indicate an error) */\ntint: red 20%;\n\n/* A green tint */\ntint: rgba(0, 200, 0, 0.3);\n
    "},{"location":"styles/tint/#python","title":"Python","text":"
    # A red tint\nfrom textual.color import Color\nwidget.styles.tint = Color.parse(\"red\").with_alpha(0.2);\n\n# A green tint\nwidget.styles.tint = \"rgba(0, 200, 0, 0.3)\"\n
    "},{"location":"styles/visibility/","title":"Visibility","text":"

    The visibility style determines whether a widget is visible or not.

    "},{"location":"styles/visibility/#syntax","title":"Syntax","text":"
    \nvisibility: hidden | visible;\n

    visibility takes one of two values to set the visibility of a widget.

    "},{"location":"styles/visibility/#values","title":"Values","text":"Value Description hidden The widget will be invisible. visible (default) The widget will be displayed as normal."},{"location":"styles/visibility/#visibility-inheritance","title":"Visibility inheritance","text":"

    Note

    Children of an invisible container can be visible.

    By default, children inherit the visibility of their parents. So, if a container is set to be invisible, its children widgets will also be invisible by default. However, those widgets can be made visible if their visibility is explicitly set to visibility: visible. This is shown in the second example below.

    "},{"location":"styles/visibility/#examples","title":"Examples","text":""},{"location":"styles/visibility/#basic-usage","title":"Basic usage","text":"

    Note that the second widget is hidden while leaving a space where it would have been rendered.

    Outputvisibility.pyvisibility.tcss

    VisibilityApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass VisibilityApp(App):\n    CSS_PATH = \"visibility.tcss\"\n\n    def compose(self):\n        yield Label(\"Widget 1\")\n        yield Label(\"Widget 2\", classes=\"invisible\")\n        yield Label(\"Widget 3\")\n\n\nif __name__ == \"__main__\":\n    app = VisibilityApp()\n    app.run()\n
    Screen {\n    background: green;\n}\n\nLabel {\n    height: 5;\n    width: 100%;\n    background: white;\n    color: blue;\n    border: heavy blue;\n}\n\nLabel.invisible {\n    visibility: hidden;\n}\n
    "},{"location":"styles/visibility/#overriding-container-visibility","title":"Overriding container visibility","text":"

    The next example shows the interaction of the visibility style with invisible containers that have visible children. The app below has three rows with a Horizontal container per row and three placeholders per row. The containers all have a white background, and then:

    • the top container is visible by default (we can see the white background around the placeholders);
    • the middle container is invisible and the children placeholders inherited that setting;
    • the bottom container is invisible but the children placeholders are visible because they were set to be visible.
    Outputvisibility_containers.pyvisibility_containers.tcss

    VisibilityContainersApp PlaceholderPlaceholderPlaceholder PlaceholderPlaceholderPlaceholder

    from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass VisibilityContainersApp(App):\n    CSS_PATH = \"visibility_containers.tcss\"\n\n    def compose(self):\n        yield VerticalScroll(\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"top\",\n            ),\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"middle\",\n            ),\n            Horizontal(\n                Placeholder(),\n                Placeholder(),\n                Placeholder(),\n                id=\"bot\",\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    app = VisibilityContainersApp()\n    app.run()\n
    Horizontal {\n    padding: 1 2;     /* (1)! */\n    background: white;\n    height: 1fr;\n}\n\n#top {}               /* (2)! */\n\n#middle {             /* (3)! */\n    visibility: hidden;\n}\n\n#bot {                /* (4)! */\n    visibility: hidden;\n}\n\n#bot > Placeholder {  /* (5)! */\n    visibility: visible;\n}\n\nPlaceholder {\n    width: 1fr;\n}\n
    1. The padding and the white background let us know when the Horizontal is visible.
    2. The top Horizontal is visible by default, and so are its children.
    3. The middle Horizontal is made invisible and its children will inherit that setting.
    4. The bottom Horizontal is made invisible...
    5. ... but its children override that setting and become visible.
    "},{"location":"styles/visibility/#css","title":"CSS","text":"
    /* Widget is invisible */\nvisibility: hidden;\n\n/* Widget is visible */\nvisibility: visible;\n
    "},{"location":"styles/visibility/#python","title":"Python","text":"
    # Widget is invisible\nself.styles.visibility = \"hidden\"\n\n# Widget is visible\nself.styles.visibility = \"visible\"\n

    There is also a shortcut to set a Widget's visibility. The visible property on Widget may be set to True or False.

    # Make a widget invisible\nwidget.visible = False\n\n# Make the widget visible again\nwidget.visible = True\n
    "},{"location":"styles/visibility/#see-also","title":"See also","text":"
    • display to specify whether a widget is displayed or not.
    "},{"location":"styles/width/","title":"Width","text":"

    The width style sets a widget's width.

    "},{"location":"styles/width/#syntax","title":"Syntax","text":"
    \nwidth: <scalar>;\n

    The style width needs a <scalar> to determine the horizontal length of the width. By default, it sets the width of the content area, but if box-sizing is set to border-box it sets the width of the border area.

    "},{"location":"styles/width/#examples","title":"Examples","text":""},{"location":"styles/width/#basic-usage","title":"Basic usage","text":"

    This example adds a widget with 50% width of the screen.

    Outputwidth.pywidth.tcss

    WidthApp Widget

    from textual.app import App\nfrom textual.widget import Widget\n\n\nclass WidthApp(App):\n    CSS_PATH = \"width.tcss\"\n\n    def compose(self):\n        yield Widget()\n\n\nif __name__ == \"__main__\":\n    app = WidthApp()\n    app.run()\n
    Screen > Widget {\n    background: green;\n    width: 50%;\n    color: white;\n}\n
    "},{"location":"styles/width/#all-width-formats","title":"All width formats","text":"Outputwidth_comparison.pywidth_comparison.tcss

    WidthComparisonApp #cells#percent#w#h#vw#vh#auto#fr1#fr3 \u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022

    from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Placeholder, Static\n\n\nclass Ruler(Static):\n    def compose(self):\n        ruler_text = \"\u00b7\u00b7\u00b7\u00b7\u2022\" * 100\n        yield Label(ruler_text)\n\n\nclass WidthComparisonApp(App):\n    CSS_PATH = \"width_comparison.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            Placeholder(id=\"cells\"),  # (1)!\n            Placeholder(id=\"percent\"),\n            Placeholder(id=\"w\"),\n            Placeholder(id=\"h\"),\n            Placeholder(id=\"vw\"),\n            Placeholder(id=\"vh\"),\n            Placeholder(id=\"auto\"),\n            Placeholder(id=\"fr1\"),\n            Placeholder(id=\"fr3\"),\n        )\n        yield Ruler()\n\n\nif __name__ == \"__main__\":\n    app = WidthComparisonApp()\n    app.run()\n
    1. The id of the placeholder identifies which unit will be used to set the width of the widget.
    #cells {\n    width: 9;      /* (1)! */\n}\n#percent {\n    width: 12.5%;  /* (2)! */\n}\n#w {\n    width: 10w;    /* (3)! */\n}\n#h {\n    width: 25h;    /* (4)! */\n}\n#vw {\n    width: 15vw;   /* (5)! */\n}\n#vh {\n    width: 25vh;   /* (6)! */\n}\n#auto {\n    width: auto;   /* (7)! */\n}\n#fr1 {\n    width: 1fr;    /* (8)! */\n}\n#fr3 {\n    width: 3fr;    /* (9)! */\n}\n\nScreen {\n    layers: ruler;\n}\n\nRuler {\n    layer: ruler;\n    dock: bottom;\n    overflow: hidden;\n    height: 1;\n    background: $accent;\n}\n
    1. This sets the width to 9 columns.
    2. This sets the width to 12.5% of the space made available by the container. The container is 80 columns wide, so 12.5% of 80 is 10.
    3. This sets the width to 10% of the width of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the width of the Horizontal is 80 and 10% of 80 is 8.
    4. This sets the width to 25% of the height of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the height of the Horizontal is 24 and 25% of 24 is 6.
    5. This sets the width to 15% of the viewport width, which is 80. 15% of 80 is 12.
    6. This sets the width to 25% of the viewport height, which is 24. 25% of 24 is 6.
    7. This sets the width of the placeholder to be the optimal size that fits the content without scrolling. Because the content is the string \"#auto\", the placeholder has its width set to 5.
    8. This sets the width to 1fr, which means this placeholder will have a third of the width of a placeholder with 3fr.
    9. This sets the width to 3fr, which means this placeholder will have triple the width of a placeholder with 1fr.
    "},{"location":"styles/width/#css","title":"CSS","text":"
    /* Explicit cell width */\nwidth: 10;\n\n/* Percentage width */\nwidth: 50%;\n\n/* Automatic width */\nwidth: auto;\n
    "},{"location":"styles/width/#python","title":"Python","text":"
    widget.styles.width = 10\nwidget.styles.width = \"50%\nwidget.styles.width = \"auto\"\n
    "},{"location":"styles/width/#see-also","title":"See also","text":"
    • max-width and min-width to limit the width of a widget.
    • height to set the height of a widget.
    "},{"location":"styles/grid/","title":"Grid","text":"

    There are a number of styles relating to the Textual grid layout.

    For an in-depth look at the grid layout, visit the grid guide.

    Property Description column-span Number of columns a cell spans. grid-columns Width of grid columns. grid-gutter Spacing between grid cells. grid-rows Height of grid rows. grid-size Number of columns and rows in the grid layout. row-span Number of rows a cell spans."},{"location":"styles/grid/#syntax","title":"Syntax","text":"
    \ncolumn-span: <integer>;\n\ngrid-columns: <scalar>+;\n\ngrid-gutter: <scalar> [<scalar>];\n\ngrid-rows: <scalar>+;\n\ngrid-size: <integer> [<integer>];\n\nrow-span: <integer>;\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/grid/#example","title":"Example","text":"

    The example below shows all the styles above in action. The grid-size: 3 4; declaration sets the grid to 3 columns and 4 rows. The first cell of the grid, tinted magenta, shows a cell spanning multiple rows and columns. The spacing between grid cells is defined by the grid-gutter style.

    Outputgrid.pygrid.tcss

    GridApp Grid\u00a0cell\u00a01Grid\u00a0cell\u00a02 row-span:\u00a03; column-span:\u00a02; Grid\u00a0cell\u00a03 Grid\u00a0cell\u00a04 Grid\u00a0cell\u00a05Grid\u00a0cell\u00a06Grid\u00a0cell\u00a07

    from textual.app import App\nfrom textual.widgets import Static\n\n\nclass GridApp(App):\n    CSS_PATH = \"grid.tcss\"\n\n    def compose(self):\n        yield Static(\"Grid cell 1\\n\\nrow-span: 3;\\ncolumn-span: 2;\", id=\"static1\")\n        yield Static(\"Grid cell 2\", id=\"static2\")\n        yield Static(\"Grid cell 3\", id=\"static3\")\n        yield Static(\"Grid cell 4\", id=\"static4\")\n        yield Static(\"Grid cell 5\", id=\"static5\")\n        yield Static(\"Grid cell 6\", id=\"static6\")\n        yield Static(\"Grid cell 7\", id=\"static7\")\n\n\nif __name__ == \"__main__\":\n    app = GridApp()\n    app.run()\n
    Screen {\n    layout: grid;\n    grid-size: 3 4;\n    grid-rows: 1fr;\n    grid-columns: 1fr;\n    grid-gutter: 1;\n}\n\nStatic {\n    color: auto;\n    background: lightblue;\n    height: 100%;\n    padding: 1 2;\n}\n\n#static1 {\n    tint: magenta 40%;\n    row-span: 3;\n    column-span: 2;\n}\n

    Warning

    The styles listed on this page will only work when the layout is grid.

    "},{"location":"styles/grid/#see-also","title":"See also","text":"
    • The grid layout guide.
    "},{"location":"styles/grid/column_span/","title":"Column-span","text":"

    The column-span style specifies how many columns a widget will span in a grid layout.

    Note

    This style only affects widgets that are direct children of a widget with layout: grid.

    "},{"location":"styles/grid/column_span/#syntax","title":"Syntax","text":"
    \ncolumn-span: <integer>;\n

    The column-span style accepts a single non-negative <integer> that quantifies how many columns the given widget spans.

    "},{"location":"styles/grid/column_span/#example","title":"Example","text":"

    The example below shows a 4 by 4 grid where many placeholders span over several columns.

    Outputcolumn_span.pycolumn_span.tcss

    MyApp #p1 #p2#p3 #p4#p5 #p6#p7

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass MyApp(App):\n    CSS_PATH = \"column_span.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(id=\"p1\"),\n            Placeholder(id=\"p2\"),\n            Placeholder(id=\"p3\"),\n            Placeholder(id=\"p4\"),\n            Placeholder(id=\"p5\"),\n            Placeholder(id=\"p6\"),\n            Placeholder(id=\"p7\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    #p1 {\n    column-span: 4;\n}\n#p2 {\n    column-span: 3;\n}\n#p3 {\n    column-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p4 {\n    column-span: 2;\n}\n#p5 {\n    column-span: 2;\n}\n#p6 {\n    /* Default value is 1. */\n}\n#p7 {\n    column-span: 3;\n}\n\nGrid {\n    grid-size: 4 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    height: 100%;\n}\n
    "},{"location":"styles/grid/column_span/#css","title":"CSS","text":"
    column-span: 3;\n
    "},{"location":"styles/grid/column_span/#python","title":"Python","text":"
    widget.styles.column_span = 3\n
    "},{"location":"styles/grid/column_span/#see-also","title":"See also","text":"
    • row-span to specify how many rows a widget spans.
    "},{"location":"styles/grid/grid_columns/","title":"Grid-columns","text":"

    The grid-columns style allows to define the width of the columns of the grid.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_columns/#syntax","title":"Syntax","text":"
    \ngrid-columns: <scalar>+;\n

    The grid-columns style takes one or more <scalar> that specify the length of the columns of the grid.

    If there are more columns in the grid than scalars specified in grid-columns, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

    "},{"location":"styles/grid/grid_columns/#example","title":"Example","text":"

    The example below shows a grid with 10 labels laid out in a grid with 2 rows and 5 columns.

    We set grid-columns: 1fr 16 2fr. Because there are more rows than scalars in the style definition, the scalars will be reused:

    • columns 1 and 4 have width 1fr;
    • columns 2 and 5 have width 16; and
    • column 3 has width 2fr.
    Outputgrid_columns.pygrid_columns.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_columns.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"2fr\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n            Label(\"2fr\"),\n            Label(\"1fr\"),\n            Label(\"width = 16\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 5 2;\n    grid-columns: 1fr 16 2fr;\n}\n\nLabel {\n    border: round white;\n    content-align-horizontal: center;\n    width: 100%;\n    height: 100%;\n}\n
    "},{"location":"styles/grid/grid_columns/#css","title":"CSS","text":"
    /* Set all columns to have 50% width */\ngrid-columns: 50%;\n\n/* Every other column is twice as wide as the first one */\ngrid-columns: 1fr 2fr;\n
    "},{"location":"styles/grid/grid_columns/#python","title":"Python","text":"
    grid.styles.grid_columns = \"50%\"\ngrid.styles.grid_columns = \"1fr 2fr\"\n
    "},{"location":"styles/grid/grid_columns/#see-also","title":"See also","text":"
    • grid-rows to specify the height of the grid rows.
    "},{"location":"styles/grid/grid_gutter/","title":"Grid-gutter","text":"

    The grid-gutter style sets the size of the gutter in the grid layout. That is, it sets the space between adjacent cells in the grid.

    Gutter is only applied between the edges of cells. No spacing is added between the edges of the cells and the edges of the container.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_gutter/#syntax","title":"Syntax","text":"
    \ngrid-gutter: <integer> [<integer>];\n

    The grid-gutter style takes one or two <integer> that set the length of the gutter along the vertical and horizontal axes. If only one <integer> is supplied, it sets the vertical and horizontal gutters. If two are supplied, they set the vertical and horizontal gutters, respectively.

    "},{"location":"styles/grid/grid_gutter/#example","title":"Example","text":"

    The example below employs a common trick to apply visually consistent spacing around all grid cells.

    Outputgrid_gutter.pygrid_gutter.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25025\u2502\u25026\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25027\u2502\u25028\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_gutter.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n            Label(\"6\"),\n            Label(\"7\"),\n            Label(\"8\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 4;\n    grid-gutter: 1 2;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. We set the horizontal gutter to be double the vertical gutter because terminal cells are typically two times taller than they are wide. Thus, the result shows visually consistent spacing around grid cells.
    "},{"location":"styles/grid/grid_gutter/#css","title":"CSS","text":"
    /* Set vertical and horizontal gutters to be the same */\ngrid-gutter: 5;\n\n/* Set vertical and horizontal gutters separately */\ngrid-gutter: 1 2;\n
    "},{"location":"styles/grid/grid_gutter/#python","title":"Python","text":"

    Vertical and horizontal gutters correspond to different Python properties, so they must be set separately:

    widget.styles.grid_gutter_vertical = \"1\"\nwidget.styles.grid_gutter_horizontal = \"2\"\n
    "},{"location":"styles/grid/grid_rows/","title":"Grid-rows","text":"

    The grid-rows style allows to define the height of the rows of the grid.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_rows/#syntax","title":"Syntax","text":"
    \ngrid-rows: <scalar>+;\n

    The grid-rows style takes one or more <scalar> that specify the length of the rows of the grid.

    If there are more rows in the grid than scalars specified in grid-rows, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

    "},{"location":"styles/grid/grid_rows/#example","title":"Example","text":"

    The example below shows a grid with 10 labels laid out in a grid with 5 rows and 2 columns.

    We set grid-rows: 1fr 6 25%. Because there are more rows than scalars in the style definition, the scalars will be reused:

    • rows 1 and 4 have height 1fr;
    • rows 2 and 5 have height 6; and
    • row 3 has height 25%.
    Outputgrid_rows.pygrid_rows.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u250225%\u2502\u250225%\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_rows.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1fr\"),\n            Label(\"1fr\"),\n            Label(\"height = 6\"),\n            Label(\"height = 6\"),\n            Label(\"25%\"),\n            Label(\"25%\"),\n            Label(\"1fr\"),\n            Label(\"1fr\"),\n            Label(\"height = 6\"),\n            Label(\"height = 6\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 5;\n    grid-rows: 1fr 6 25%;\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    "},{"location":"styles/grid/grid_rows/#css","title":"CSS","text":"
    /* Set all rows to have 50% height */\ngrid-rows: 50%;\n\n/* Every other row is twice as tall as the first one */\ngrid-rows: 1fr 2fr;\n
    "},{"location":"styles/grid/grid_rows/#python","title":"Python","text":"
    grid.styles.grid_rows = \"50%\"\ngrid.styles.grid_rows = \"1fr 2fr\"\n
    "},{"location":"styles/grid/grid_rows/#see-also","title":"See also","text":"
    • grid-columns to specify the width of the grid columns.
    "},{"location":"styles/grid/grid_size/","title":"Grid-size","text":"

    The grid-size style sets the number of columns and rows in a grid layout.

    The number of rows can be left unspecified and it will be computed automatically.

    Note

    This style only affects widgets with layout: grid.

    "},{"location":"styles/grid/grid_size/#syntax","title":"Syntax","text":"
    \ngrid-size: <integer> [<integer>];\n

    The grid-size style takes one or two non-negative <integer>. The first defines how many columns there are in the grid. If present, the second one sets the number of rows \u2013 regardless of the number of children of the grid \u2013, otherwise the number of rows is computed automatically.

    "},{"location":"styles/grid/grid_size/#examples","title":"Examples","text":""},{"location":"styles/grid/grid_size/#columns-and-rows","title":"Columns and rows","text":"

    In the first example, we create a grid with 2 columns and 5 rows, although we do not have enough labels to fill in the whole grid:

    Outputgrid_size_both.pygrid_size_both.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_size_both.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2 4;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. Create a grid with 2 columns and 4 rows.
    "},{"location":"styles/grid/grid_size/#columns-only","title":"Columns only","text":"

    In the second example, we create a grid with 2 columns and however many rows are needed to display all of the grid children:

    Outputgrid_size_columns.pygrid_size_columns.tcss

    MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\n\n\nclass MyApp(App):\n    CSS_PATH = \"grid_size_columns.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Label(\"1\"),\n            Label(\"2\"),\n            Label(\"3\"),\n            Label(\"4\"),\n            Label(\"5\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    Grid {\n    grid-size: 2;  /* (1)! */\n}\n\nLabel {\n    border: round white;\n    content-align: center middle;\n    width: 100%;\n    height: 100%;\n}\n
    1. Create a grid with 2 columns and however many rows.
    "},{"location":"styles/grid/grid_size/#css","title":"CSS","text":"
    /* Grid with 3 rows and 5 columns */\ngrid-size: 3 5;\n\n/* Grid with 4 columns and as many rows as needed */\ngrid-size: 4;\n
    "},{"location":"styles/grid/grid_size/#python","title":"Python","text":"

    To programmatically change the grid size, the number of rows and columns must be specified separately:

    widget.styles.grid_size_rows = 3\nwidget.styles.grid_size_columns = 6\n
    "},{"location":"styles/grid/row_span/","title":"Row-span","text":"

    The row-span style specifies how many rows a widget will span in a grid layout.

    Note

    This style only affects widgets that are direct children of a widget with layout: grid.

    "},{"location":"styles/grid/row_span/#syntax","title":"Syntax","text":"
    \nrow-span: <integer>;\n

    The row-span style accepts a single non-negative <integer> that quantifies how many rows the given widget spans.

    "},{"location":"styles/grid/row_span/#example","title":"Example","text":"

    The example below shows a 4 by 4 grid where many placeholders span over several rows.

    Notice that grid cells are filled from left to right, top to bottom. After placing the placeholders #p1, #p2, #p3, and #p4, the next available cell is in the second row, fourth column, which is where the top of #p5 is.

    Outputrow_span.pyrow_span.tcss

    MyApp #p4 #p3 #p2 #p1 #p5 #p6 #p7

    from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\n\n\nclass MyApp(App):\n    CSS_PATH = \"row_span.tcss\"\n\n    def compose(self):\n        yield Grid(\n            Placeholder(id=\"p1\"),\n            Placeholder(id=\"p2\"),\n            Placeholder(id=\"p3\"),\n            Placeholder(id=\"p4\"),\n            Placeholder(id=\"p5\"),\n            Placeholder(id=\"p6\"),\n            Placeholder(id=\"p7\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n
    #p1 {\n    row-span: 4;\n}\n#p2 {\n    row-span: 3;\n}\n#p3 {\n    row-span: 2;\n}\n#p4 {\n    row-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p5 {\n    row-span: 3;\n}\n#p6 {\n    row-span: 2;\n}\n#p7 {\n    /* Default value is 1. */\n}\n\nGrid {\n    grid-size: 4 4;\n    grid-gutter: 1 2;\n}\n\nPlaceholder {\n    height: 100%;\n}\n
    "},{"location":"styles/grid/row_span/#css","title":"CSS","text":"
    row-span: 3\n
    "},{"location":"styles/grid/row_span/#python","title":"Python","text":"
    widget.styles.row_span = 3\n
    "},{"location":"styles/grid/row_span/#see-also","title":"See also","text":"
    • column-span to specify how many columns a widget spans.
    "},{"location":"styles/links/","title":"Links","text":"

    Textual supports the concept of inline \"links\" embedded in text which trigger an action when pressed. There are a number of styles which influence the appearance of these links within a widget.

    Note

    These CSS rules only target Textual action links. Internet hyperlinks are not affected by these styles.

    Property Description link-background The background color of the link text. link-background-hover The background color of the link text when the cursor is over it. link-color The color of the link text. link-color-hover The color of the link text when the cursor is over it. link-style The style of the link text (e.g. underline). link-style-hover The style of the link text when the cursor is over it."},{"location":"styles/links/#syntax","title":"Syntax","text":"
    \nlink-background: <color> [<percentage>];\n\nlink-color: <color> [<percentage>];\n\nlink-style: <text-style>;\n\nlink-background-hover: <color> [<percentage>];\n\nlink-color-hover: <color> [<percentage>];\n\nlink-style-hover: <text-style>;\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/links/#example","title":"Example","text":"

    In the example below, the first label illustrates default link styling. The second label uses CSS to customize the link color, background, and style.

    Outputlinks.pylinks.tcss

    LinksApp Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click! Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\nTEXT = \"\"\"\\\nHere is a [@click='app.bell']link[/] which you can click!\n\"\"\"\n\n\nclass LinksApp(App):\n    CSS_PATH = \"links.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Static(TEXT)\n        yield Static(TEXT, id=\"custom\")\n\n\nif __name__ == \"__main__\":\n    app = LinksApp()\n    app.run()\n
    #custom {\n    link-color: black 90%;\n    link-background: dodgerblue;\n    link-style: bold italic underline;\n}\n
    "},{"location":"styles/links/#additional-notes","title":"Additional Notes","text":"
    • Inline links are not widgets, and thus cannot be focused.
    "},{"location":"styles/links/#see-also","title":"See Also","text":"
    • An introduction to links in the Actions guide.
    "},{"location":"styles/links/link_background/","title":"Link-background","text":"

    The link-background style sets the background color of the link.

    Note

    link-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_background/#syntax","title":"Syntax","text":"
    \nlink-background: <color> [<percentage>];\n

    link-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links.

    "},{"location":"styles/links/link_background/#example","title":"Example","text":"

    The example below shows some links with their background color changed. It also shows that link-background does not affect hyperlinks.

    Outputlink_background.pylink_background.tcss

    LinkBackgroundApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkBackgroundApp(App):\n    CSS_PATH = \"link_background.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkBackgroundApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-background rule.
    2. This label has an \"action link\" that can be styled with link-background.
    3. This label has an \"action link\" that can be styled with link-background.
    4. This label has an \"action link\" that can be styled with link-background.
    #lbl1, #lbl2 {\n    link-background: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-background: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-background: $accent;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_background/#css","title":"CSS","text":"
    link-background: red 70%;\nlink-background: $accent;\n
    "},{"location":"styles/links/link_background/#python","title":"Python","text":"
    widget.styles.link_background = \"red 70%\"\nwidget.styles.link_background = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_background = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_background/#see-also","title":"See also","text":"
    • link-color to set the color of link text.
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_background_hover/","title":"Link-background-hover","text":"

    The link-background-hover style sets the background color of the link when the mouse cursor is over the link.

    Note

    link-background-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_background_hover/#syntax","title":"Syntax","text":"
    \nlink-background-hover: <color> [<percentage>];\n

    link-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.

    "},{"location":"styles/links/link_background_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-background-hover set to $accent.

    "},{"location":"styles/links/link_background_hover/#example","title":"Example","text":"

    The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for link-background-hover.

    It also shows that link-background-hover does not affect hyperlinks.

    Outputlink_background_hover.pylink_background_hover.tcss

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_background_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverBackgroundApp(App):\n    CSS_PATH = \"link_background_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverBackgroundApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-background-hover rule.
    2. This label has an \"action link\" that can be styled with link-background-hover.
    3. This label has an \"action link\" that can be styled with link-background-hover.
    4. This label has an \"action link\" that can be styled with link-background-hover.
    #lbl1, #lbl2 {\n    link-background-hover: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-background-hover: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    /* Empty to show the default hover background */ /* (2)! */\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    2. The default behavior for links on hover is to change to a different background color, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
    "},{"location":"styles/links/link_background_hover/#css","title":"CSS","text":"
    link-background-hover: red 70%;\nlink-background-hover: $accent;\n
    "},{"location":"styles/links/link_background_hover/#python","title":"Python","text":"
    widget.styles.link_background_hover = \"red 70%\"\nwidget.styles.link_background_hover = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_background_hover = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_background_hover/#see-also","title":"See also","text":"
    • link-background to set the background color of link text.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_color/","title":"Link-color","text":"

    The link-color style sets the color of the link text.

    Note

    link-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_color/#syntax","title":"Syntax","text":"
    \nlink-color: <color> [<percentage>];\n

    link-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links.

    "},{"location":"styles/links/link_color/#example","title":"Example","text":"

    The example below shows some links with their color changed. It also shows that link-color does not affect hyperlinks.

    Outputlink_color.pylink_color.tcss

    LinkColorApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkColorApp(App):\n    CSS_PATH = \"link_color.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkColorApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-color rule.
    2. This label has an \"action link\" that can be styled with link-color.
    3. This label has an \"action link\" that can be styled with link-color.
    4. This label has an \"action link\" that can be styled with link-color.
    #lbl1, #lbl2 {\n    link-color: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-color: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-color: $accent;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_color/#css","title":"CSS","text":"
    link-color: red 70%;\nlink-color: $accent;\n
    "},{"location":"styles/links/link_color/#python","title":"Python","text":"
    widget.styles.link_color = \"red 70%\"\nwidget.styles.link_color = \"$accent\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_color = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_color/#see-also","title":"See also","text":"
    • link-background to set the background color of link text.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_color_hover/","title":"Link-color-hover","text":"

    The link-color-hover style sets the color of the link text when the mouse cursor is over the link.

    Note

    link-color-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_color_hover/#syntax","title":"Syntax","text":"
    \nlink-color-hover: <color> [<percentage>];\n

    link-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.

    "},{"location":"styles/links/link_color_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-color-hover set to white.

    "},{"location":"styles/links/link_color_hover/#example","title":"Example","text":"

    The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-color-hover does not affect hyperlinks.

    Outputlink_color_hover.pylink_color_hover.tcss

    Note

    The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-background-hover but we haven't done so in this example.

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_color_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverColorApp(App):\n    CSS_PATH = \"link_color_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverColorApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-color-hover rule.
    2. This label has an \"action link\" that can be styled with link-color-hover.
    3. This label has an \"action link\" that can be styled with link-color-hover.
    4. This label has an \"action link\" that can be styled with link-color-hover.
    #lbl1, #lbl2 {\n    link-color-hover: red;  /* (1)! */\n}\n\n#lbl3 {\n    link-color-hover: hsl(60,100%,50%) 50%;\n}\n\n#lbl4 {\n    link-color-hover: black;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_color_hover/#css","title":"CSS","text":"
    link-color-hover: red 70%;\nlink-color-hover: black;\n
    "},{"location":"styles/links/link_color_hover/#python","title":"Python","text":"
    widget.styles.link_color_hover = \"red 70%\"\nwidget.styles.link_color_hover = \"black\"\n\n# You can also use a `Color` object directly:\nwidget.styles.link_color_hover = Color(100, 30, 173)\n
    "},{"location":"styles/links/link_color_hover/#see-also","title":"See also","text":"
    • link-color to set the color of link text.
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    "},{"location":"styles/links/link_style/","title":"Link-style","text":"

    The link-style style sets the text style for the link text.

    Note

    link-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_style/#syntax","title":"Syntax","text":"
    \nlink-style: <text-style>;\n

    link-style will take all the values specified and will apply that styling to text that is enclosed by a Textual action link.

    "},{"location":"styles/links/link_style/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-style set to underline.

    "},{"location":"styles/links/link_style/#example","title":"Example","text":"

    The example below shows some links with different styles applied to their text. It also shows that link-style does not affect hyperlinks.

    Outputlink_style.pylink_style.tcss

    LinkStyleApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkStyleApp(App):\n    CSS_PATH = \"link_style.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkStyleApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-style rule.
    2. This label has an \"action link\" that can be styled with link-style.
    3. This label has an \"action link\" that can be styled with link-style.
    4. This label has an \"action link\" that can be styled with link-style.
    #lbl1, #lbl2 {\n    link-style: bold italic;  /* (1)! */\n}\n\n#lbl3 {\n    link-style: reverse strike;\n}\n\n#lbl4 {\n    link-style: bold;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    "},{"location":"styles/links/link_style/#css","title":"CSS","text":"
    link-style: bold;\nlink-style: bold italic reverse;\n
    "},{"location":"styles/links/link_style/#python","title":"Python","text":"
    widget.styles.link_style = \"bold\"\nwidget.styles.link_style = \"bold italic reverse\"\n
    "},{"location":"styles/links/link_style/#see-also","title":"See also","text":"
    • `link-style-hover to set the style of link text when the mouse pointer is over it.
    • text-style to set the style of text in a widget.
    "},{"location":"styles/links/link_style_hover/","title":"Link-style-hover","text":"

    The link-style-hover style sets the text style for the link text when the mouse cursor is over the link.

    Note

    link-style-hover only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

    "},{"location":"styles/links/link_style_hover/#syntax","title":"Syntax","text":"
    \nlink-style-hover: <text-style>;\n

    link-style-hover applies its <text-style> to the text of Textual action links when the mouse pointer is over them.

    "},{"location":"styles/links/link_style_hover/#defaults","title":"Defaults","text":"

    If not provided, a Textual action link will have link-style-hover set to bold.

    "},{"location":"styles/links/link_style_hover/#example","title":"Example","text":"

    The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-style-hover does not affect hyperlinks.

    Outputlink_style_hover.pylink_style_hover.tcss

    Note

    The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-background-hover but we haven't done so in this example.

    Note

    The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_style_hover.py.

    from textual.app import App\nfrom textual.widgets import Label\n\n\nclass LinkHoverStyleApp(App):\n    CSS_PATH = \"link_style_hover.tcss\"\n\n    def compose(self):\n        yield Label(\n            \"Visit the [link=https://textualize.io]Textualize[/link] website.\",\n            id=\"lbl1\",  # (1)!\n        )\n        yield Label(\n            \"Click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl2\",  # (2)!\n        )\n        yield Label(\n            \"You can also click [@click=app.bell]here[/] for the bell sound.\",\n            id=\"lbl3\",  # (3)!\n        )\n        yield Label(\n            \"[@click=app.quit]Exit this application.[/]\",\n            id=\"lbl4\",  # (4)!\n        )\n\n\nif __name__ == \"__main__\":\n    app = LinkHoverStyleApp()\n    app.run()\n
    1. This label has a hyperlink so it won't be affected by the link-style-hover rule.
    2. This label has an \"action link\" that can be styled with link-style-hover.
    3. This label has an \"action link\" that can be styled with link-style-hover.
    4. This label has an \"action link\" that can be styled with link-style-hover.
    #lbl1, #lbl2 {\n    link-style-hover: bold italic;  /* (1)! */\n}\n\n#lbl3 {\n    link-style-hover: reverse strike;\n}\n\n#lbl4 {\n    link-style-hover: bold;\n}\n
    1. This will only affect one of the labels because action links are the only links that this rule affects.
    2. The default behavior for links on hover is to change to a different text style, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
    "},{"location":"styles/links/link_style_hover/#css","title":"CSS","text":"
    link-style-hover: bold;\nlink-style-hover: bold italic reverse;\n
    "},{"location":"styles/links/link_style_hover/#python","title":"Python","text":"
    widget.styles.link_style_hover = \"bold\"\nwidget.styles.link_style_hover = \"bold italic reverse\"\n
    "},{"location":"styles/links/link_style_hover/#see-also","title":"See also","text":"
    • `link-background-hover to set the background color of link text when the mouse pointer is over it.
    • `link-color-hover to set the color of link text when the mouse pointer is over it.
    • link-style to set the style of link text.
    • text-style to set the style of text in a widget.
    "},{"location":"styles/scrollbar_colors/","title":"Scrollbar colors","text":"

    There are a number of styles to set the colors used in Textual scrollbars. You won't typically need to do this, as the default themes have carefully chosen colors, but you can if you want to.

    Style Applies to scrollbar-background Scrollbar background. scrollbar-background-active Scrollbar background when the thumb is being dragged. scrollbar-background-hover Scrollbar background when the mouse is hovering over it. scrollbar-color Scrollbar \"thumb\" (movable part). scrollbar-color-active Scrollbar thumb when it is active (being dragged). scrollbar-color-hover Scrollbar thumb when the mouse is hovering over it. scrollbar-corner-color The gap between the horizontal and vertical scrollbars."},{"location":"styles/scrollbar_colors/#syntax","title":"Syntax","text":"
    \nscrollbar-background: <color> [<percentage>];\n\nscrollbar-background-active: <color> [<percentage>];\n\nscrollbar-background-hover: <color> [<percentage>];\n\nscrollbar-color: <color> [<percentage>];\n\nscrollbar-color-active: <color> [<percentage>];\n\nscrollbar-color-hover: <color> [<percentage>];\n\nscrollbar-corner-color: <color> [<percentage>];\n

    Visit each style's reference page to learn more about how the values are used.

    "},{"location":"styles/scrollbar_colors/#example","title":"Example","text":"

    This example shows two panels that contain oversized text. The right panel sets scrollbar-background, scrollbar-color, and scrollbar-corner-color, and the left panel shows the default colors for comparison.

    Outputscrollbars.pyscrollbars.tcss

    ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.\u2583\u2583see\u00a0its\u00a0path.\u2583\u2583 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t \u258d\u258d

    from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarApp(App):\n    CSS_PATH = \"scrollbars.tcss\"\n\n    def compose(self):\n        yield Horizontal(\n            ScrollableContainer(Label(TEXT * 10)),\n            ScrollableContainer(Label(TEXT * 10), classes=\"right\"),\n        )\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarApp()\n    app.run()\n
    Label {\n    width: 150%;\n    height: 150%;\n}\n\n.right {\n    scrollbar-background: red;\n    scrollbar-color: green;\n    scrollbar-corner-color: blue;\n}\n\nHorizontal > ScrollableContainer {\n    width: 50%;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/","title":"Scrollbar-background","text":"

    The scrollbar-background style sets the background color of the scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_background/#syntax","title":"Syntax","text":"
    \nscrollbar-background: <color> [<percentage>];\n

    scrollbar-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_background/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#css","title":"CSS","text":"
    scrollbar-backround: blue;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#python","title":"Python","text":"
    widget.styles.scrollbar_background = \"blue\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background/#see-also","title":"See also","text":"
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/","title":"Scrollbar-background-active","text":"

    The scrollbar-background-active style sets the background color of the scrollbar when the thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#syntax","title":"Syntax","text":"
    \nscrollbar-background-active: <color> [<percentage>];\n

    scrollbar-background-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when its thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#css","title":"CSS","text":"
    scrollbar-backround-active: red;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#python","title":"Python","text":"
    widget.styles.scrollbar_background_active = \"red\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_active/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/","title":"Scrollbar-background-hover","text":"

    The scrollbar-background-hover style sets the background color of the scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#syntax","title":"Syntax","text":"
    \nscrollbar-background-hover: <color> [<percentage>];\n

    scrollbar-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#css","title":"CSS","text":"
    scrollbar-background-hover: purple;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#python","title":"Python","text":"
    widget.styles.scrollbar_background_hover = \"purple\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also","title":"See also","text":""},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also_1","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    "},{"location":"styles/scrollbar_colors/scrollbar_color/","title":"Scrollbar-color","text":"

    The scrollbar-color style sets the color of the scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_color/#syntax","title":"Syntax","text":"
    \nscrollbar-color: <color> [<percentage>];\n

    scrollbar-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar.

    "},{"location":"styles/scrollbar_colors/scrollbar_color/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#css","title":"CSS","text":"
    scrollbar-color: cyan;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#python","title":"Python","text":"
    widget.styles.scrollbar_color = \"cyan\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/","title":"Scrollbar-color-active","text":"

    The scrollbar-color-active style sets the color of the scrollbar when the thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#syntax","title":"Syntax","text":"
    \nscrollbar-color-active: <color> [<percentage>];\n

    scrollbar-color-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when its thumb is being dragged.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#css","title":"CSS","text":"
    scrollbar-color-active: yellow;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#python","title":"Python","text":"
    widget.styles.scrollbar_color_active = \"yellow\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_active/#see-also","title":"See also","text":"
    • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/","title":"Scrollbar-color-hover","text":"

    The scrollbar-color-hover style sets the color of the scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#syntax","title":"Syntax","text":"
    \nscrollbar-color-hover: <color> [<percentage>];\n

    scrollbar-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when the cursor is over it.

    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

    Note

    The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass Scrollbar2App(App):\n    CSS_PATH = \"scrollbars2.tcss\"\n\n    def compose(self):\n        yield Label(TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = Scrollbar2App()\n    app.run()\n
    Screen {\n    scrollbar-background: blue;\n    scrollbar-background-active: red;\n    scrollbar-background-hover: purple;\n    scrollbar-color: cyan;\n    scrollbar-color-active: yellow;\n    scrollbar-color-hover: pink;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#css","title":"CSS","text":"
    scrollbar-color-hover: pink;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#python","title":"Python","text":"
    widget.styles.scrollbar_color_hover = \"pink\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#see-also","title":"See also","text":"
    • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
    • scrollbar-color to set the color of scrollbars.
    • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/","title":"Scrollbar-corner-color","text":"

    The scrollbar-corner-color style sets the color of the gap between the horizontal and vertical scrollbars.

    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#syntax","title":"Syntax","text":"
    \nscrollbar-corner-color: <color> [<percentage>];\n

    scrollbar-corner-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of the gap between the horizontal and vertical scrollbars of a widget.

    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#example","title":"Example","text":"

    The example below sets the scrollbar corner (bottom-right corner of the screen) to white.

    Outputscrollbar_corner_color.pyscrollbar_corner_color.tcss

    ScrollbarCornerColorApp I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.

    from textual.app import App\nfrom textual.widgets import Label\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\n\n\nclass ScrollbarCornerColorApp(App):\n    CSS_PATH = \"scrollbar_corner_color.tcss\"\n\n    def compose(self):\n        yield Label(TEXT.replace(\"\\n\", \" \") + \"\\n\" + TEXT * 10)\n\n\nif __name__ == \"__main__\":\n    app = ScrollbarCornerColorApp()\n    app.run()\n
    Screen {\n    overflow: auto auto;\n    scrollbar-corner-color: white;\n}\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#css","title":"CSS","text":"
    scrollbar-corner-color: white;\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#python","title":"Python","text":"
    widget.styles.scrollbar_corner_color = \"white\"\n
    "},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#see-also","title":"See also","text":"
    • scrollbar-background to set the background color of scrollbars.
    • scrollbar-color to set the color of scrollbars.
    "},{"location":"widgets/","title":"Widgets","text":"

    A reference to the builtin widgets.

    See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

    "},{"location":"widgets/button/","title":"Button","text":"

    A simple button widget which can be pressed using a mouse click or by pressing Enter when it has focus.

    • Focusable
    • Container
    "},{"location":"widgets/button/#example","title":"Example","text":"

    The example below shows each button variant, and its disabled equivalent. Clicking any of the non-disabled buttons in the example app below will result in the app exiting and the details of the selected button being printed to the console.

    Outputbutton.pybutton.tcss

    ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DefaultDefault \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Primary!Primary! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Success!Success! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Warning!Warning! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Error!Error! \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, Static\n\n\nclass ButtonsApp(App[str]):\n    CSS_PATH = \"button.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Horizontal(\n            VerticalScroll(\n                Static(\"Standard Buttons\", classes=\"header\"),\n                Button(\"Default\"),\n                Button(\"Primary!\", variant=\"primary\"),\n                Button.success(\"Success!\"),\n                Button.warning(\"Warning!\"),\n                Button.error(\"Error!\"),\n            ),\n            VerticalScroll(\n                Static(\"Disabled Buttons\", classes=\"header\"),\n                Button(\"Default\", disabled=True),\n                Button(\"Primary!\", variant=\"primary\", disabled=True),\n                Button.success(\"Success!\", disabled=True),\n                Button.warning(\"Warning!\", disabled=True),\n                Button.error(\"Error!\", disabled=True),\n            ),\n        )\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.exit(str(event.button))\n\n\nif __name__ == \"__main__\":\n    app = ButtonsApp()\n    print(app.run())\n
    Button {\n    margin: 1 2;\n}\n\nHorizontal > VerticalScroll {\n    width: 24;\n}\n\n.header {\n    margin: 1 0 0 2;\n    text-style: bold;\n}\n
    "},{"location":"widgets/button/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description label str \"\" The text that appears inside the button. variant ButtonVariant \"default\" Semantic styling variant. One of default, primary, success, warning, error. disabled bool False Whether the button is disabled or not. Disabled buttons cannot be focused or clicked, and are styled in a way that suggests this."},{"location":"widgets/button/#messages","title":"Messages","text":"
    • Button.Pressed
    "},{"location":"widgets/button/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/button/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/button/#additional-notes","title":"Additional Notes","text":"
    • The spacing between the text and the edges of a button are not due to padding. The default styling for a Button has the height set to 3 lines and a min-width of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with border: none;.
    "},{"location":"widgets/button/#textual.widgets.Button","title":"textual.widgets.Button class","text":"
    def __init__(\n    self,\n    label=None,\n    variant=\"default\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A simple clickable button.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    variant ButtonVariant 'default'

    The variant of the button.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    "},{"location":"widgets/button/#textual.widgets._button.Button.active_effect_duration","title":"active_effect_duration instance-attribute","text":"
    active_effect_duration = 0.3\n

    Amount of time in seconds the button 'press' animation lasts.

    "},{"location":"widgets/button/#textual.widgets._button.Button.label","title":"label class-attribute instance-attribute","text":"
    label: reactive[TextType] = label\n

    The text label that appears within the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.variant","title":"variant class-attribute instance-attribute","text":"
    variant = variant\n

    The variant name for the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed","title":"Pressed class","text":"
    def __init__(self, button):\n

    Bases: Message

    Event sent when a Button is pressed.

    Can be handled using on_button_pressed in a subclass of Button or in a parent widget in the DOM.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.button","title":"button instance-attribute","text":"
    button: Button = button\n

    The button that was pressed.

    "},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.control","title":"control property","text":"
    control: Button\n

    An alias for Pressed.button.

    This will be the same value as Pressed.button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.action_press","title":"action_press method","text":"
    def action_press(self):\n

    Activate a press of the button.

    "},{"location":"widgets/button/#textual.widgets._button.Button.error","title":"error classmethod","text":"
    def error(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating an error Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'error' variant.

    "},{"location":"widgets/button/#textual.widgets._button.Button.press","title":"press method","text":"
    def press(self):\n

    Respond to a button press.

    Returns Type Description Self

    The button instance.

    "},{"location":"widgets/button/#textual.widgets._button.Button.success","title":"success classmethod","text":"
    def success(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating a success Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'success' variant.

    "},{"location":"widgets/button/#textual.widgets._button.Button.validate_label","title":"validate_label method","text":"
    def validate_label(self, label):\n

    Parse markup for self.label

    "},{"location":"widgets/button/#textual.widgets._button.Button.warning","title":"warning classmethod","text":"
    def warning(\n    cls,\n    label=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Utility constructor for creating a warning Button variant.

    Parameters Parameter Default Description label TextType | None None

    The text that appears within the button.

    disabled bool False

    Whether the button is disabled or not.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    Returns Type Description Button

    A Button widget of the 'warning' variant.

    "},{"location":"widgets/button/#textual.widgets.button","title":"textual.widgets.button","text":""},{"location":"widgets/button/#textual.widgets.button.ButtonVariant","title":"ButtonVariant module-attribute","text":"
    ButtonVariant = Literal[\n    \"default\", \"primary\", \"success\", \"warning\", \"error\"\n]\n

    The names of the valid button variants.

    These are the variants that can be used with a Button.

    "},{"location":"widgets/checkbox/","title":"Checkbox","text":"

    Added in version 0.13.0

    A simple checkbox widget which stores a boolean value.

    • Focusable
    • Container
    "},{"location":"widgets/checkbox/#example","title":"Example","text":"

    The example below shows check boxes in various states.

    Outputcheckbox.pycheckbox.tcss

    CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Checkbox\n\n\nclass CheckboxApp(App[None]):\n    CSS_PATH = \"checkbox.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            yield Checkbox(\"Arrakis :sweat:\")\n            yield Checkbox(\"Caladan\")\n            yield Checkbox(\"Chusuk\")\n            yield Checkbox(\"[b]Giedi Prime[/b]\")\n            yield Checkbox(\"[magenta]Ginaz[/]\")\n            yield Checkbox(\"Grumman\", True)\n            yield Checkbox(\"Kaitain\", id=\"initial_focus\")\n            yield Checkbox(\"Novebruns\", True)\n\n    def on_mount(self):\n        self.query_one(\"#initial_focus\", Checkbox).focus()\n\n\nif __name__ == \"__main__\":\n    CheckboxApp().run()\n
    Screen {\n    align: center middle;\n}\n\nVerticalScroll {\n    width: auto;\n    height: auto;\n    background: $boost;\n    padding: 2;\n}\n
    "},{"location":"widgets/checkbox/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the checkbox."},{"location":"widgets/checkbox/#messages","title":"Messages","text":"
    • Checkbox.Changed
    "},{"location":"widgets/checkbox/#bindings","title":"Bindings","text":"

    The checkbox widget defines the following bindings:

    Key(s) Description enter, space Toggle the value."},{"location":"widgets/checkbox/#component-classes","title":"Component Classes","text":"

    The checkbox widget inherits the following component classes:

    Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/checkbox/#textual.widgets.Checkbox","title":"textual.widgets.Checkbox class","text":"

    Bases: ToggleButton

    A check box widget that represents a boolean value.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed","title":"Changed class","text":"

    Bases: ToggleButton.Changed

    Posted when the value of the checkbox changes.

    This message can be handled using an on_checkbox_changed method.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.checkbox","title":"checkbox property","text":"
    checkbox: Checkbox\n

    The checkbox that was changed.

    "},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.control","title":"control property","text":"
    control: Checkbox\n

    An alias for Changed.checkbox.

    "},{"location":"widgets/collapsible/","title":"Collapsible","text":"

    Added in version 0.37

    A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing Enter.

    • Focusable
    • Container
    "},{"location":"widgets/collapsible/#composing","title":"Composing","text":"

    You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (with statement).

    Here is an example of using the constructor to add content:

    def compose(self) -> ComposeResult:\n    yield Collapsible(Label(\"Hello, world.\"))\n

    Here's how the to use it with the context manager:

    def compose(self) -> ComposeResult:\n    with Collapsible():\n        yield Label(\"Hello, world.\")\n

    The second form is generally preferred, but the end result is the same.

    "},{"location":"widgets/collapsible/#title","title":"Title","text":"

    The default title \"Toggle\" can be customized by setting the title parameter of the constructor:

    def compose(self) -> ComposeResult:\n    with Collapsible(title=\"An interesting story.\"):\n        yield Label(\"Interesting but verbose story.\")\n
    "},{"location":"widgets/collapsible/#initial-state","title":"Initial State","text":"

    The initial state of the Collapsible widget can be customized via the collapsed parameter of the constructor:

    def compose(self) -> ComposeResult:\n    with Collapsible(title=\"Contents 1\", collapsed=False):\n        yield Label(\"Hello, world.\")\n\n    with Collapsible(title=\"Contents 2\", collapsed=True):  # Default.\n        yield Label(\"Hello, world.\")\n
    "},{"location":"widgets/collapsible/#collapseexpand-symbols","title":"Collapse/Expand Symbols","text":"

    The symbols used to show the collapsed / expanded state can be customized by setting the parameters collapsed_symbol and expanded_symbol:

    def compose(self) -> ComposeResult:\n    with Collapsible(collapsed_symbol=\">>>\", expanded_symbol=\"v\"):\n        yield Label(\"Hello, world.\")\n
    "},{"location":"widgets/collapsible/#examples","title":"Examples","text":"

    The following example contains three Collapsibles in different states.

    All expandedAll collapsedMixedcollapsible.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Paul \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Leto \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Jessica \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Footer, Label, Markdown\n\nLETO = \"\"\"\\\n# Duke Leto I Atreides\n\nHead of House Atreides.\"\"\"\n\nJESSICA = \"\"\"\n# Lady Jessica\n\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\n\nPAUL = \"\"\"\n# Paul Atreides\n\nSon of Leto and Jessica.\n\"\"\"\n\n\nclass CollapsibleApp(App[None]):\n    \"\"\"An example of collapsible container.\"\"\"\n\n    BINDINGS = [\n        (\"c\", \"collapse_or_expand(True)\", \"Collapse All\"),\n        (\"e\", \"collapse_or_expand(False)\", \"Expand All\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Compose app with collapsible containers.\"\"\"\n        yield Footer()\n        with Collapsible(collapsed=False, title=\"Leto\"):\n            yield Label(LETO)\n        yield Collapsible(Markdown(JESSICA), collapsed=False, title=\"Jessica\")\n        with Collapsible(collapsed=True, title=\"Paul\"):\n            yield Markdown(PAUL)\n\n    def action_collapse_or_expand(self, collapse: bool) -> None:\n        for child in self.walk_children(Collapsible):\n            child.collapsed = collapse\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#setting-initial-state","title":"Setting Initial State","text":"

    The example below shows nested Collapsible widgets and how to set their initial state.

    Outputcollapsible_nested.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Toggle \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Toggle

    from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Label\n\n\nclass CollapsibleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        with Collapsible(collapsed=False):\n            with Collapsible():\n                yield Label(\"Hello, world.\")\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#custom-symbols","title":"Custom Symbols","text":"

    The following example shows Collapsible widgets with custom expand/collapse symbols.

    Outputcollapsible_custom_symbol.py

    CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 >>>\u00a0Togglev\u00a0Toggle Hello,\u00a0world.

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Collapsible, Label\n\n\nclass CollapsibleApp(App[None]):\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            with Collapsible(\n                collapsed_symbol=\">>>\",\n                expanded_symbol=\"v\",\n            ):\n                yield Label(\"Hello, world.\")\n\n            with Collapsible(\n                collapsed_symbol=\">>>\",\n                expanded_symbol=\"v\",\n                collapsed=False,\n            ):\n                yield Label(\"Hello, world.\")\n\n\nif __name__ == \"__main__\":\n    app = CollapsibleApp()\n    app.run()\n
    "},{"location":"widgets/collapsible/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description collapsed bool True Controls the collapsed/expanded state of the widget. title str \"Toggle\" Title of the collapsed/expanded contents."},{"location":"widgets/collapsible/#messages","title":"Messages","text":"
    • Collapsible.Toggled
    "},{"location":"widgets/collapsible/#bindings","title":"Bindings","text":"

    The collapsible widget defines the following binding on its title:

    Key(s) Description enter Toggle the collapsible."},{"location":"widgets/collapsible/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/collapsible/#textual.widgets.Collapsible","title":"textual.widgets.Collapsible class","text":"
    def __init__(\n    self,\n    *children,\n    title=\"Toggle\",\n    collapsed=True,\n    collapsed_symbol=\"\u25b6\",\n    expanded_symbol=\"\u25bc\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A collapsible container.

    Parameters Parameter Default Description *children Widget ()

    Contents that will be collapsed/expanded.

    title str 'Toggle'

    Title of the collapsed/expanded contents.

    collapsed bool True

    Default status of the contents.

    collapsed_symbol str '\u25b6'

    Collapsed symbol before the title.

    expanded_symbol str '\u25bc'

    Expanded symbol before the title.

    name str | None None

    The name of the collapsible.

    id str | None None

    The ID of the collapsible in the DOM.

    classes str | None None

    The CSS classes of the collapsible.

    disabled bool False

    Whether the collapsible is disabled or not.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Collapsed","title":"Collapsed class","text":"

    Bases: Toggled

    Event sent when the Collapsible widget is collapsed.

    Can be handled using on_collapsible_collapsed in a subclass of Collapsible or in a parent widget in the DOM.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Expanded","title":"Expanded class","text":"

    Bases: Toggled

    Event sent when the Collapsible widget is expanded.

    Can be handled using on_collapsible_expanded in a subclass of Collapsible or in a parent widget in the DOM.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled","title":"Toggled class","text":"
    def __init__(self, collapsible):\n

    Bases: Message

    Parent class subclassed by Collapsible messages.

    Can be handled with on(Collapsible.Toggled) if you want to handle expansions and collapsed in the same way, or you can handle the specific events individually.

    Parameters Parameter Default Description collapsible Collapsible required

    The Collapsible widget that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled.collapsible","title":"collapsible instance-attribute","text":"
    collapsible: Collapsible = collapsible\n

    The collapsible that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets._collapsible.Collapsible.Toggled.control","title":"control property","text":"
    control: Collapsible\n

    An alias for Toggled.collapsible.

    "},{"location":"widgets/content_switcher/","title":"ContentSwitcher","text":"

    Added in version 0.14.0

    A widget for containing and switching display between multiple child widgets.

    • Focusable
    • Container
    "},{"location":"widgets/content_switcher/#example","title":"Example","text":"

    The example below uses a ContentSwitcher in combination with two Buttons to create a simple tabbed view. Note how each Button has an ID set, and how each child of the ContentSwitcher has a corresponding ID; then a Button.Clicked handler is used to set ContentSwitcher.current to switch between the different views.

    Outputcontent_switcher.pycontent_switcher.tcss

    ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DataTableMarkdown \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u00a0Book\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Year\u00a0\u2502 \u2502\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01965\u00a0\u2502 \u2502\u00a0Dune\u00a0Messiah\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01969\u00a0\u2502 \u2502\u00a0Children\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01976\u00a0\u2502 \u2502\u00a0God\u00a0Emperor\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01981\u00a0\u2502 \u2502\u00a0Heretics\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01984\u00a0\u2502 \u2502\u00a0Chapterhouse:\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01985\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, ContentSwitcher, DataTable, Markdown\n\nMARKDOWN_EXAMPLE = \"\"\"# Three Flavours Cornetto\n\nThe Three Flavours Cornetto trilogy is an anthology series of British\ncomedic genre films directed by Edgar Wright.\n\n## Shaun of the Dead\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Strawberry | 2004-04-09 | Edgar Wright |\n\n## Hot Fuzz\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Classico | 2007-02-17 | Edgar Wright |\n\n## The World's End\n\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Mint | 2013-07-19 | Edgar Wright |\n\"\"\"\n\n\nclass ContentSwitcherApp(App[None]):\n    CSS_PATH = \"content_switcher.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal(id=\"buttons\"):  # (1)!\n            yield Button(\"DataTable\", id=\"data-table\")  # (2)!\n            yield Button(\"Markdown\", id=\"markdown\")  # (3)!\n\n        with ContentSwitcher(initial=\"data-table\"):  # (4)!\n            yield DataTable(id=\"data-table\")\n            with VerticalScroll(id=\"markdown\"):\n                yield Markdown(MARKDOWN_EXAMPLE)\n\n    def on_button_pressed(self, event: Button.Pressed) -> None:\n        self.query_one(ContentSwitcher).current = event.button.id  # (5)!\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(\"Book\", \"Year\")\n        table.add_rows(\n            [\n                (title.ljust(35), year)\n                for title, year in (\n                    (\"Dune\", 1965),\n                    (\"Dune Messiah\", 1969),\n                    (\"Children of Dune\", 1976),\n                    (\"God Emperor of Dune\", 1981),\n                    (\"Heretics of Dune\", 1984),\n                    (\"Chapterhouse: Dune\", 1985),\n                )\n            ]\n        )\n\n\nif __name__ == \"__main__\":\n    ContentSwitcherApp().run()\n
    1. A Horizontal to hold the buttons, each with a unique ID.
    2. This button will select the DataTable in the ContentSwitcher.
    3. This button will select the Markdown in the ContentSwitcher.
    4. Note that the initial visible content is set by its ID, see below.
    5. When a button is pressed, its ID is used to switch to a different widget in the ContentSwitcher. Remember that IDs are unique within parent, so the buttons and the widgets in the ContentSwitcher can share IDs.
    Screen {\n    align: center middle;\n    padding: 1;\n}\n\n#buttons {\n    height: 3;\n    width: auto;\n}\n\nContentSwitcher {\n    background: $panel;\n    border: round $primary;\n    width: 90%;\n    height: 1fr;\n}\n\nDataTable {\n    background: $panel;\n}\n\nMarkdownH2 {\n    background: $primary;\n    color: yellow;\n    border: none;\n    padding: 0;\n}\n

    When the user presses the \"Markdown\" button the view is switched:

    ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 DataTableMarkdown \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eThree\u00a0Flavours\u00a0Cornetto\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u2502 \u2502of\u00a0British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Shaun\u00a0of\u00a0the\u00a0Dead\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eStrawberry\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hot\u00a0Fuzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eClassico\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0The\u00a0World's\u00a0End\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

    "},{"location":"widgets/content_switcher/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description current str | None None The ID of the currently-visible child. None means nothing is visible."},{"location":"widgets/content_switcher/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/content_switcher/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/content_switcher/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
    def __init__(\n    self,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    initial=None\n):\n

    Bases: Container

    A widget for switching between different children.

    Note

    All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

    Parameters Parameter Default Description *children Widget ()

    The widgets to switch between.

    name str | None None

    The name of the content switcher.

    id str | None None

    The ID of the content switcher in the DOM.

    classes str | None None

    The CSS classes of the content switcher.

    disabled bool False

    Whether the content switcher is disabled or not.

    initial str | None None

    The ID of the initial widget to show, None or empty string for the first tab.

    Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
    current: reactive[str | None] = reactive[Optional[str]](\n    None, init=False\n)\n

    The ID of the currently-displayed widget.

    If set to None then no widget is visible.

    Note

    If set to an unknown ID, this will result in NoMatches being raised.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content: Widget | None\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
    def watch_current(self, old, new):\n

    React to the current visible child choice being changed.

    Parameters Parameter Default Description old str | None required

    The old widget ID (or None if there was no widget).

    new str | None required

    The new widget ID (or None if nothing should be shown).

    "},{"location":"widgets/data_table/","title":"DataTable","text":"

    A table widget optimized for displaying a lot of data.

    • Focusable
    • Container
    "},{"location":"widgets/data_table/#guide","title":"Guide","text":""},{"location":"widgets/data_table/#adding-data","title":"Adding data","text":"

    The following example shows how to fill a table with data. First, we use add_columns to include the lane, swimmer, country, and time columns in the table. After that, we use the add_rows method to insert the rows into the table.

    Outputdata_table.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        table.add_rows(ROWS[1:])\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    To add a single row or column use add_row and add_column, respectively.

    "},{"location":"widgets/data_table/#styling-and-justifying-cells","title":"Styling and justifying cells","text":"

    Cells can contain more than just plain strings - Rich renderables such as Text are also supported. Text objects provide an easy way to style and justify cell content:

    Outputdata_table_renderables.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0Singapore50.39 \u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0PhelpsUnited\u00a0States51.14 \u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0South\u00a0Africa51.14 \u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary51.14 \u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China51.26 \u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France51.58 \u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0ShieldsUnited\u00a0States51.73 \u00a0\u00a0\u00a01Aleksandr\u00a0Sadovnikov\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Russia51.84 \u00a0\u00a010\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0Scotland51.84

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        for row in ROWS[1:]:\n            # Adding styled and justified `Text` objects instead of plain strings.\n            styled_row = [\n                Text(str(cell), style=\"italic #03AC13\", justify=\"right\") for cell in row\n            ]\n            table.add_row(*styled_row)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#keys","title":"Keys","text":"

    When adding a row to the table, you can supply a key to add_row. A key is a unique identifier for that row. If you don't supply a key, Textual will generate one for you and return it from add_row. This key can later be used to reference the row, regardless of its current position in the table.

    When working with data from a database, for example, you may wish to set the row key to the primary key of the data to ensure uniqueness. The method add_column also accepts a key argument and works similarly.

    Keys are important because cells in a data table can change location due to factors like row deletion and sorting. Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table.

    If you want to change the table based solely on coordinates, you can use the coordinate_to_cell_key method to convert a coordinate to a cell key, which is a (row_key, column_key) pair.

    "},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

    The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute. Three types of cursors are supported: cell, row, and column. Change the cursor type by assigning to the cursor_type reactive attribute.

    Column CursorRow CursorCell Cursordata_table_cursors.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from itertools import cycle\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\ncursors = cycle([\"column\", \"row\", \"cell\"])\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.cursor_type = next(cursors)\n        table.zebra_stripes = True\n        table.add_columns(*ROWS[0])\n        table.add_rows(ROWS[1:])\n\n    def key_c(self):\n        table = self.query_one(DataTable)\n        table.cursor_type = next(cursors)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    You can change the position of the cursor using the arrow keys, Page Up, Page Down, Home and End, or by assigning to the cursor_coordinate reactive attribute.

    "},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

    Cells can be updated in the DataTable by using the update_cell and update_cell_at methods.

    "},{"location":"widgets/data_table/#removing-data","title":"Removing data","text":"

    To remove all data in the table, use the clear method. To remove individual rows, use remove_row. The remove_row method accepts a key argument, which identifies the row to be removed.

    If you wish to remove the row below the cursor in the DataTable, use coordinate_to_cell_key to get the row key of the row under the current cursor_coordinate, then supply this key to remove_row:

    # Get the keys for the row and column under the cursor.\nrow_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the row key to `remove_row` to delete the row.\ntable.remove_row(row_key)\n
    "},{"location":"widgets/data_table/#removing-columns","title":"Removing columns","text":"

    To remove individual columns, use remove_column. The remove_column method accepts a key argument, which identifies the column to be removed.

    You can remove the column below the cursor using the same coordinate_to_cell_key method described above:

    # Get the keys for the row and column under the cursor.\n_, column_key = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the column key to `column_row` to delete the column.\ntable.remove_column(column_key)\n
    "},{"location":"widgets/data_table/#fixed-data","title":"Fixed data","text":"

    You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. To do this, assign an integer to the fixed_rows or fixed_columns reactive attributes of the DataTable.

    Fixed datadata_table_fixed.py

    TableApp \u00a0A\u00a0\u00a0\u00a0B\u00a0\u00a0\u00a0\u00a0C\u00a0\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0 \u00a079\u00a0\u00a0158\u00a0\u00a0237\u00a0 \u00a080\u00a0\u00a0160\u00a0\u00a0240\u00a0 \u00a081\u00a0\u00a0162\u00a0\u00a0243\u00a0 \u00a082\u00a0\u00a0164\u00a0\u00a0246\u00a0 \u00a083\u00a0\u00a0166\u00a0\u00a0249\u00a0 \u00a084\u00a0\u00a0168\u00a0\u00a0252\u00a0 \u00a085\u00a0\u00a0170\u00a0\u00a0255\u00a0 \u00a086\u00a0\u00a0172\u00a0\u00a0258\u00a0 \u00a087\u00a0\u00a0174\u00a0\u00a0261\u00a0 \u00a088\u00a0\u00a0176\u00a0\u00a0264\u00a0 \u00a089\u00a0\u00a0178\u00a0\u00a0267\u00a0 \u00a090\u00a0\u00a0180\u00a0\u00a0270\u00a0 \u00a091\u00a0\u00a0182\u00a0\u00a0273\u00a0 \u00a092\u00a0\u00a0184\u00a0\u00a0276\u00a0 \u00a093\u00a0\u00a0186\u00a0\u00a0279\u00a0 \u00a094\u00a0\u00a0188\u00a0\u00a0282\u00a0\u2587\u2587 \u00a095\u00a0\u00a0190\u00a0\u00a0285\u00a0 \u00a096\u00a0\u00a0192\u00a0\u00a0288\u00a0 \u00a097\u00a0\u00a0194\u00a0\u00a0291\u00a0 \u00a098\u00a0\u00a0196\u00a0\u00a0294\u00a0 \u00a099\u00a0\u00a0198\u00a0\u00a0297\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\n\nclass TableApp(App):\n    CSS = \"DataTable {height: 1fr}\"\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.focus()\n        table.add_columns(\"A\", \"B\", \"C\")\n        for number in range(1, 100):\n            table.add_row(str(number), str(number * 2), str(number * 3))\n        table.fixed_rows = 2\n        table.fixed_columns = 1\n        table.cursor_type = \"row\"\n        table.zebra_stripes = True\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n

    In the example above, we set fixed_rows to 2, and fixed_columns to 1, meaning the first two rows and the leftmost column do not scroll - they always remain visible as you scroll through the data table.

    "},{"location":"widgets/data_table/#sorting","title":"Sorting","text":"

    The DataTable can be sorted using the sort method. In order to sort your data by a column, you can provide the key you supplied to the add_column method or a ColumnKey. You can then pass one more column keys to the sort method to sort by one or more columns.

    Additionally, you can sort your DataTable with a custom function (or other callable) via the key argument. Similar to the key parameter of the built-in sorted() function, your function (or other callable) should take a single argument (row) and return a key to use for sorting purposes.

    Providing both columns and key will limit the row information sent to your key function (or other callable) to only the columns specified.

    Outputdata_table_sort.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a01\u00a0\u00a0time\u00a02\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a050.39\u00a0\u00a0\u00a051.84\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a050.39\u00a0\u00a0\u00a051.84\u00a0\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a051.14\u00a0\u00a0\u00a051.73\u00a0\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a051.14\u00a0\u00a0\u00a051.58\u00a0\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a051.26\u00a0\u00a0\u00a051.26\u00a0\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a051.58\u00a0\u00a0\u00a052.15\u00a0\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a051.73\u00a0\u00a0\u00a051.12\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0Russia\u00a051.84\u00a0\u00a0\u00a050.85\u00a0\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a051.84\u00a0\u00a0\u00a051.55\u00a0\u00a0 \u00a0A\u00a0\u00a0Sort\u00a0By\u00a0Average\u00a0Time\u00a0\u00a0N\u00a0\u00a0Sort\u00a0By\u00a0Last\u00a0Name\u00a0\u00a0C\u00a0\u00a0Sort\u00a0By\u00a0Country\u00a0\u00a0D\u00a0\u00a0Sort\u00a0By\u00a0\u2026

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable, Footer\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time 1\", \"time 2\"),\n    (4, \"Joseph Schooling\", Text(\"Singapore\", style=\"italic\"), 50.39, 51.84),\n    (2, \"Michael Phelps\", Text(\"United States\", style=\"italic\"), 50.39, 51.84),\n    (5, \"Chad le Clos\", Text(\"South Africa\", style=\"italic\"), 51.14, 51.73),\n    (6, \"L\u00e1szl\u00f3 Cseh\", Text(\"Hungary\", style=\"italic\"), 51.14, 51.58),\n    (3, \"Li Zhuhao\", Text(\"China\", style=\"italic\"), 51.26, 51.26),\n    (8, \"Mehdy Metella\", Text(\"France\", style=\"italic\"), 51.58, 52.15),\n    (7, \"Tom Shields\", Text(\"United States\", style=\"italic\"), 51.73, 51.12),\n    (1, \"Aleksandr Sadovnikov\", Text(\"Russia\", style=\"italic\"), 51.84, 50.85),\n    (10, \"Darren Burns\", Text(\"Scotland\", style=\"italic\"), 51.84, 51.55),\n]\n\n\nclass TableApp(App):\n    BINDINGS = [\n        (\"a\", \"sort_by_average_time\", \"Sort By Average Time\"),\n        (\"n\", \"sort_by_last_name\", \"Sort By Last Name\"),\n        (\"c\", \"sort_by_country\", \"Sort By Country\"),\n        (\"d\", \"sort_by_columns\", \"Sort By Columns (Only)\"),\n    ]\n\n    current_sorts: set = set()\n\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        for col in ROWS[0]:\n            table.add_column(col, key=col)\n        table.add_rows(ROWS[1:])\n\n    def sort_reverse(self, sort_type: str):\n        \"\"\"Determine if `sort_type` is ascending or descending.\"\"\"\n        reverse = sort_type in self.current_sorts\n        if reverse:\n            self.current_sorts.remove(sort_type)\n        else:\n            self.current_sorts.add(sort_type)\n        return reverse\n\n    def action_sort_by_average_time(self) -> None:\n        \"\"\"Sort DataTable by average of times (via a function) and\n        passing of column data through positional arguments.\"\"\"\n\n        def sort_by_average_time_then_last_name(row_data):\n            name, *scores = row_data\n            return (sum(scores) / len(scores), name.split()[-1])\n\n        table = self.query_one(DataTable)\n        table.sort(\n            \"swimmer\",\n            \"time 1\",\n            \"time 2\",\n            key=sort_by_average_time_then_last_name,\n            reverse=self.sort_reverse(\"time\"),\n        )\n\n    def action_sort_by_last_name(self) -> None:\n        \"\"\"Sort DataTable by last name of swimmer (via a lambda).\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\n            \"swimmer\",\n            key=lambda swimmer: swimmer.split()[-1],\n            reverse=self.sort_reverse(\"swimmer\"),\n        )\n\n    def action_sort_by_country(self) -> None:\n        \"\"\"Sort DataTable by country which is a `Rich.Text` object.\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\n            \"country\",\n            key=lambda country: country.plain,\n            reverse=self.sort_reverse(\"country\"),\n        )\n\n    def action_sort_by_columns(self) -> None:\n        \"\"\"Sort DataTable without a key.\"\"\"\n        table = self.query_one(DataTable)\n        table.sort(\"swimmer\", \"lane\", reverse=self.sort_reverse(\"columns\"))\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#labelled-rows","title":"Labelled rows","text":"

    A \"label\" can be attached to a row using the add_row method. This will add an extra column to the left of the table which the cursor cannot interact with. This column is similar to the leftmost column in a spreadsheet containing the row numbers. The example below shows how to attach simple numbered labels to rows.

    Labelled rowsdata_table_labels.py

    TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 1\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 2\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 3\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 4\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 5\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 6\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 7\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 8\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 9\u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

    from rich.text import Text\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\n\nROWS = [\n    (\"lane\", \"swimmer\", \"country\", \"time\"),\n    (4, \"Joseph Schooling\", \"Singapore\", 50.39),\n    (2, \"Michael Phelps\", \"United States\", 51.14),\n    (5, \"Chad le Clos\", \"South Africa\", 51.14),\n    (6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n    (3, \"Li Zhuhao\", \"China\", 51.26),\n    (8, \"Mehdy Metella\", \"France\", 51.58),\n    (7, \"Tom Shields\", \"United States\", 51.73),\n    (1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n    (10, \"Darren Burns\", \"Scotland\", 51.84),\n]\n\n\nclass TableApp(App):\n    def compose(self) -> ComposeResult:\n        yield DataTable()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(*ROWS[0])\n        for number, row in enumerate(ROWS[1:], start=1):\n            label = Text(str(number), style=\"#B0FC38 italic\")\n            table.add_row(*row, label=label)\n\n\napp = TableApp()\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/data_table/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_header bool True Show the table header show_row_labels bool True Show the row labels (if applicable) fixed_rows int 0 Number of fixed rows (rows which do not scroll) fixed_columns int 0 Number of fixed columns (columns which do not scroll) zebra_stripes bool False Display alternating colors on rows header_height int 1 Height of header row show_cursor bool True Show the cursor cursor_type str \"cell\" One of \"cell\", \"row\", \"column\", or \"none\" cursor_coordinate Coordinate Coordinate(0, 0) The current coordinate of the cursor hover_coordinate Coordinate Coordinate(0, 0) The coordinate the mouse cursor is above"},{"location":"widgets/data_table/#messages","title":"Messages","text":"
    • DataTable.CellHighlighted
    • DataTable.CellSelected
    • DataTable.RowHighlighted
    • DataTable.RowSelected
    • DataTable.ColumnHighlighted
    • DataTable.ColumnSelected
    • DataTable.HeaderSelected
    • DataTable.RowLabelSelected
    "},{"location":"widgets/data_table/#bindings","title":"Bindings","text":"

    The data table widget defines the following bindings:

    Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#component-classes","title":"Component Classes","text":"

    The data table widget provides the following component classes:

    Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets.DataTable","title":"textual.widgets.DataTable class","text":"
    def __init__(\n    self,\n    *,\n    show_header=True,\n    show_row_labels=True,\n    fixed_rows=0,\n    fixed_columns=0,\n    zebra_stripes=False,\n    header_height=1,\n    show_cursor=True,\n    cursor_foreground_priority=\"css\",\n    cursor_background_priority=\"renderable\",\n    cursor_type=\"cell\",\n    cell_padding=_DEFAULT_CELL_X_PADDING,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView, Generic[CellType]

    A tabular widget that contains data.

    Parameters Parameter Default Description show_header bool True

    Whether the table header should be visible or not.

    show_row_labels bool True

    Whether the row labels should be shown or not.

    fixed_rows int 0

    The number of rows, counting from the top, that should be fixed and still visible when the user scrolls down.

    fixed_columns int 0

    The number of columns, counting from the left, that should be fixed and still visible when the user scrolls right.

    zebra_stripes bool False

    Enables or disables a zebra effect applied to the background color of the rows of the table, where alternate colors are styled differently to improve the readability of the table.

    header_height int 1

    The height, in number of cells, of the data table header.

    show_cursor bool True

    Whether the cursor should be visible when navigating the data table or not.

    cursor_foreground_priority Literal['renderable', 'css'] 'css'

    If the data associated with a cell is an arbitrary renderable with a set foreground color, this determines whether that color is prioritized over the cursor component class or not.

    cursor_background_priority Literal['renderable', 'css'] 'renderable'

    If the data associated with a cell is an arbitrary renderable with a set background color, this determines whether that color is prioritized over the cursor component class or not.

    cursor_type CursorType 'cell'

    The type of cursor to be used when navigating the data table with the keyboard.

    cell_padding int _DEFAULT_CELL_X_PADDING

    The number of cells added on each side of each column. Setting this value to zero will likely make your table very heard to read.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"Cursor Right\", show=False\n    ),\n    Binding(\n        \"left\", \"cursor_left\", \"Cursor Left\", show=False\n    ),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"datatable--cursor\",\n    \"datatable--hover\",\n    \"datatable--fixed\",\n    \"datatable--fixed-cursor\",\n    \"datatable--header\",\n    \"datatable--header-cursor\",\n    \"datatable--header-hover\",\n    \"datatable--odd-row\",\n    \"datatable--even-row\",\n}\n
    Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cell_padding","title":"cell_padding class-attribute instance-attribute","text":"
    cell_padding = cell_padding\n

    Horizontal padding between cells, applied on each side of each cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.columns","title":"columns instance-attribute","text":"
    columns: dict[ColumnKey, Column] = {}\n

    Metadata about the columns of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_background_priority","title":"cursor_background_priority instance-attribute","text":"
    cursor_background_priority = cursor_background_priority\n

    Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_column","title":"cursor_column property","text":"
    cursor_column: int\n

    The index of the column that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_coordinate","title":"cursor_coordinate class-attribute instance-attribute","text":"
    cursor_coordinate: Reactive[Coordinate] = Reactive(\n    Coordinate(0, 0), repaint=False, always_update=True\n)\n

    Current cursor Coordinate.

    This can be set programmatically or changed via the method move_cursor.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_foreground_priority","title":"cursor_foreground_priority instance-attribute","text":"
    cursor_foreground_priority = cursor_foreground_priority\n

    Should we prioritize the cursor component class CSS foreground or the renderable foreground in the event where a cell contains a renderable with a foreground color.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_row","title":"cursor_row property","text":"
    cursor_row: int\n

    The index of the row that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_type","title":"cursor_type class-attribute instance-attribute","text":"
    cursor_type: Reactive[CursorType] = cursor_type\n

    The type of cursor of the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_columns","title":"fixed_columns class-attribute instance-attribute","text":"
    fixed_columns = fixed_columns\n

    The number of columns to fix (prevented from scrolling).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_rows","title":"fixed_rows class-attribute instance-attribute","text":"
    fixed_rows = fixed_rows\n

    The number of rows to fix (prevented from scrolling).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.header_height","title":"header_height class-attribute instance-attribute","text":"
    header_height = header_height\n

    The height of the header row (the row of column labels).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_column","title":"hover_column property","text":"
    hover_column: int\n

    The index of the column that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_coordinate","title":"hover_coordinate class-attribute instance-attribute","text":"
    hover_coordinate: Reactive[Coordinate] = Reactive(\n    Coordinate(0, 0), repaint=False, always_update=True\n)\n

    The coordinate of the DataTable that is being hovered.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_row","title":"hover_row property","text":"
    hover_row: int\n

    The index of the row that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_columns","title":"ordered_columns property","text":"
    ordered_columns: list[Column]\n

    The list of Columns in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_rows","title":"ordered_rows property","text":"
    ordered_rows: list[Row]\n

    The list of Rows in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.row_count","title":"row_count property","text":"
    row_count: int\n

    The number of rows currently present in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.rows","title":"rows instance-attribute","text":"
    rows: dict[RowKey, Row] = {}\n

    Metadata about the rows of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_cursor","title":"show_cursor class-attribute instance-attribute","text":"
    show_cursor = show_cursor\n

    Show/hide both the keyboard and hover cursor.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_header","title":"show_header class-attribute instance-attribute","text":"
    show_header = show_header\n

    Show/hide the header row (the row of column labels).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_row_labels","title":"show_row_labels class-attribute instance-attribute","text":"
    show_row_labels = show_row_labels\n

    Show/hide the column containing the labels of rows.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.zebra_stripes","title":"zebra_stripes class-attribute instance-attribute","text":"
    zebra_stripes = zebra_stripes\n

    Apply zebra effect on row backgrounds (light, dark, light, dark, ...).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted","title":"CellHighlighted class","text":"
    def __init__(self, data_table, value, coordinate, cell_key):\n

    Bases: Message

    Posted when the cursor moves to highlight a new cell.

    This is only relevant when the cursor_type is \"cell\". It's also posted when the cell cursor is re-enabled (by setting show_cursor=True), and when the cursor type is changed to \"cell\". Can be handled using on_data_table_cell_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
    cell_key: CellKey = cell_key\n

    The key for the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
    coordinate: Coordinate = coordinate\n

    The coordinate of the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
    value: CellType = value\n

    The value in the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected","title":"CellSelected class","text":"
    def __init__(self, data_table, value, coordinate, cell_key):\n

    Bases: Message

    Posted by the DataTable widget when a cell is selected.

    This is only relevant when the cursor_type is \"cell\". Can be handled using on_data_table_cell_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
    cell_key: CellKey = cell_key\n

    The key for the selected cell.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
    coordinate: Coordinate = coordinate\n

    The coordinate of the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.value","title":"value instance-attribute","text":"
    value: CellType = value\n

    The value in the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted","title":"ColumnHighlighted class","text":"
    def __init__(self, data_table, cursor_column, column_key):\n

    Bases: Message

    Posted when a column is highlighted.

    This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column: int = cursor_column\n

    The x-coordinate of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected","title":"ColumnSelected class","text":"
    def __init__(self, data_table, cursor_column, column_key):\n

    Bases: Message

    Posted when a column is selected.

    This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column: int = cursor_column\n

    The x-coordinate of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected","title":"HeaderSelected class","text":"
    def __init__(self, data_table, column_key, column_index, label):\n

    Bases: Message

    Posted when a column header/label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_index","title":"column_index instance-attribute","text":"
    column_index = column_index\n

    The index for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_key","title":"column_key instance-attribute","text":"
    column_key = column_key\n

    The key for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted","title":"RowHighlighted class","text":"
    def __init__(self, data_table, cursor_row, row_key):\n

    Bases: Message

    Posted when a row is highlighted.

    This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_highlighted in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row: int = cursor_row\n

    The y-coordinate of the cursor that highlighted the row.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey = row_key\n

    The key of the row that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected","title":"RowLabelSelected class","text":"
    def __init__(self, data_table, row_key, row_index, label):\n

    Bases: Message

    Posted when a row label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_index","title":"row_index instance-attribute","text":"
    row_index = row_index\n

    The index for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key for the column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected","title":"RowSelected class","text":"
    def __init__(self, data_table, cursor_row, row_key):\n

    Bases: Message

    Posted when a row is selected.

    This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_selected in a subclass of DataTable or in a parent widget in the DOM.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.control","title":"control property","text":"
    control: DataTable\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row: int = cursor_row\n

    The y-coordinate of the cursor that made the selection.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey = row_key\n

    The key of the row that was selected.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the cursor one page down.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the cursor one page up.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_end","title":"action_scroll_end method","text":"
    def action_scroll_end(self):\n

    Scroll to the bottom of the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_home","title":"action_scroll_home method","text":"
    def action_scroll_home(self):\n

    Scroll to the top of the data table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_column","title":"add_column method","text":"
    def add_column(\n    self, label, *, width=None, key=None, default=None\n):\n

    Add a column to the table.

    Parameters Parameter Default Description label TextType required

    A str or Text object containing the label (shown top of column).

    width int | None None

    Width of the column in cells or None to fit content.

    key str | None None

    A key which uniquely identifies this column. If None, it will be generated for you.

    default CellType | None None

    The value to insert into pre-existing rows.

    Returns Type Description ColumnKey

    Uniquely identifies this column. Can be used to retrieve this column regardless of its current location in the DataTable (it could have moved after being added due to sorting/insertion/deletion of other columns).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_columns","title":"add_columns method","text":"
    def add_columns(self, *labels):\n

    Add a number of columns.

    Parameters Parameter Default Description *labels TextType ()

    Column headers.

    Returns Type Description list[ColumnKey]

    A list of the keys for the columns that were added. See the add_column method docstring for more information on how these keys are used.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_row","title":"add_row method","text":"
    def add_row(self, *cells, height=1, key=None, label=None):\n

    Add a row at the bottom of the DataTable.

    Parameters Parameter Default Description *cells CellType ()

    Positional arguments should contain cell data.

    height int | None 1

    The height of a row (in lines). Use None to auto-detect the optimal height.

    key str | None None

    A key which uniquely identifies this row. If None, it will be generated for you and returned.

    label TextType | None None

    The label for the row. Will be displayed to the left if supplied.

    Returns Type Description RowKey

    Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows).

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_rows","title":"add_rows method","text":"
    def add_rows(self, rows):\n

    Add a number of rows at the bottom of the DataTable.

    Parameters Parameter Default Description rows Iterable[Iterable[CellType]] required

    Iterable of rows. A row is an iterable of cells.

    Returns Type Description list[RowKey]

    A list of the keys for the rows that were added. See the add_row method docstring for more information on how these keys are used.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.clear","title":"clear method","text":"
    def clear(self, columns=False):\n

    Clear the table.

    Parameters Parameter Default Description columns bool False

    Also clear the columns.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key method","text":"
    def coordinate_to_cell_key(self, coordinate):\n

    Return the key for the cell currently occupying this coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to exam the current cell key of.

    Returns Type Description CellKey

    The key of the cell currently occupying this coordinate.

    Raises Type Description CellDoesNotExist

    If the coordinate is not valid.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell","title":"get_cell method","text":"
    def get_cell(self, row_key, column_key):\n

    Given a row key and column key, return the value of the corresponding cell.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key of the cell.

    column_key ColumnKey | str required

    The column key of the cell.

    Returns Type Description CellType

    The value of the cell identified by the row and column keys.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_at","title":"get_cell_at method","text":"
    def get_cell_at(self, coordinate):\n

    Get the value from the cell occupying the given coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to retrieve the value from.

    Returns Type Description CellType

    The value of the cell at the coordinate.

    Raises Type Description CellDoesNotExist

    If there is no cell with the given coordinate.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_coordinate","title":"get_cell_coordinate method","text":"
    def get_cell_coordinate(self, row_key, column_key):\n

    Given a row key and column key, return the corresponding cell coordinate.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key of the cell.

    column_key ColumnKey | str required

    The column key of the cell.

    Returns Type Description Coordinate

    The current coordinate of the cell identified by the row and column keys.

    Raises Type Description CellDoesNotExist

    If the specified cell does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column","title":"get_column method","text":"
    def get_column(self, column_key):\n

    Get the values from the column identified by the given column key.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The key of the column.

    Returns Type Description Iterable[CellType]

    A generator which yields the cells in the column.

    Raises Type Description ColumnDoesNotExist

    If there is no column corresponding to the key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_at","title":"get_column_at method","text":"
    def get_column_at(self, column_index):\n

    Get the values from the column at a given index.

    Parameters Parameter Default Description column_index int required

    The index of the column.

    Returns Type Description Iterable[CellType]

    A generator which yields the cells in the column.

    Raises Type Description ColumnDoesNotExist

    If there is no column with the given index.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_index","title":"get_column_index method","text":"
    def get_column_index(self, column_key):\n

    Return the current index for the column identified by column_key.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The column key to find the current index of.

    Returns Type Description int

    The current index of the specified column key.

    Raises Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row","title":"get_row method","text":"
    def get_row(self, row_key):\n

    Get the values from the row identified by the given row key.

    Parameters Parameter Default Description row_key RowKey | str required

    The key of the row.

    Returns Type Description list[CellType]

    A list of the values contained within the row.

    Raises Type Description RowDoesNotExist

    When there is no row corresponding to the key.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_at","title":"get_row_at method","text":"
    def get_row_at(self, row_index):\n

    Get the values from the cells in a row at a given index. This will return the values from a row based on the rows current position in the table.

    Parameters Parameter Default Description row_index int required

    The index of the row.

    Returns Type Description list[CellType]

    A list of the values contained in the row.

    Raises Type Description RowDoesNotExist

    If there is no row with the given index.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_height","title":"get_row_height method","text":"
    def get_row_height(self, row_key):\n

    Given a row key, return the height of that row in terminal cells.

    Parameters Parameter Default Description row_key RowKey required

    The key of the row.

    Returns Type Description int

    The height of the row, measured in terminal character cells.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_index","title":"get_row_index method","text":"
    def get_row_index(self, row_key):\n

    Return the current index for the row identified by row_key.

    Parameters Parameter Default Description row_key RowKey | str required

    The row key to find the current index of.

    Returns Type Description int

    The current index of the specified row key.

    Raises Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_column_index","title":"is_valid_column_index method","text":"
    def is_valid_column_index(self, column_index):\n

    Return a boolean indicating whether the column_index is within table bounds.

    Parameters Parameter Default Description column_index int required

    The column index to check.

    Returns Type Description bool

    True if the column index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_coordinate","title":"is_valid_coordinate method","text":"
    def is_valid_coordinate(self, coordinate):\n

    Return a boolean indicating whether the given coordinate is valid.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to validate.

    Returns Type Description bool

    True if the coordinate is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_row_index","title":"is_valid_row_index method","text":"
    def is_valid_row_index(self, row_index):\n

    Return a boolean indicating whether the row_index is within table bounds.

    Parameters Parameter Default Description row_index int required

    The row index to check.

    Returns Type Description bool

    True if the row index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.move_cursor","title":"move_cursor method","text":"
    def move_cursor(self, *, row=None, column=None, animate=False):\n

    Move the cursor to the given position.

    Example
    datatable = app.query_one(DataTable)\ndatatable.move_cursor(row=4, column=6)\n# datatable.cursor_coordinate == Coordinate(4, 6)\ndatatable.move_cursor(row=3)\n# datatable.cursor_coordinate == Coordinate(3, 6)\n
    Parameters Parameter Default Description row int | None None

    The new row to move the cursor to.

    column int | None None

    The new column to move the cursor to.

    animate bool False

    Whether to animate the change of coordinates.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_column","title":"refresh_column method","text":"
    def refresh_column(self, column_index):\n

    Refresh the column at the given index.

    Parameters Parameter Default Description column_index int required

    The index of the column to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_coordinate","title":"refresh_coordinate method","text":"
    def refresh_coordinate(self, coordinate):\n

    Refresh the cell at a coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_row","title":"refresh_row method","text":"
    def refresh_row(self, row_index):\n

    Refresh the row at the given index.

    Parameters Parameter Default Description row_index int required

    The index of the row to refresh.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_column","title":"remove_column method","text":"
    def remove_column(self, column_key):\n

    Remove a column (identified by a key) from the DataTable.

    Parameters Parameter Default Description column_key ColumnKey | str required

    The key identifying the column to remove.

    Raises Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_row","title":"remove_row method","text":"
    def remove_row(self, row_key):\n

    Remove a row (identified by a key) from the DataTable.

    Parameters Parameter Default Description row_key RowKey | str required

    The key identifying the row to remove.

    Raises Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.sort","title":"sort method","text":"
    def sort(self, *columns, key=None, reverse=False):\n

    Sort the rows in the DataTable by one or more column keys or a key function (or other callable). If both columns and a key function are specified, only data from those columns will sent to the key function.

    Parameters Parameter Default Description columns ColumnKey | str ()

    One or more columns to sort by the values in.

    key Callable[[Any], Any] | None None

    A function (or other callable) that returns a key to use for sorting purposes.

    reverse bool False

    If True, the sort order will be reversed.

    Returns Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell","title":"update_cell method","text":"
    def update_cell(\n    self, row_key, column_key, value, *, update_width=False\n):\n

    Update the cell identified by the specified row key and column key.

    Parameters Parameter Default Description row_key RowKey | str required

    The key identifying the row.

    column_key ColumnKey | str required

    The key identifying the column.

    value CellType required

    The new value to put inside the cell.

    update_width bool False

    Whether to resize the column width to accommodate for the new cell content.

    Raises Type Description CellDoesNotExist

    When the supplied row_key and column_key cannot be found in the table.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell_at","title":"update_cell_at method","text":"
    def update_cell_at(\n    self, coordinate, value, *, update_width=False\n):\n

    Update the content inside the cell currently occupying the given coordinate.

    Parameters Parameter Default Description coordinate Coordinate required

    The coordinate to update the cell at.

    value CellType required

    The new value to place inside the cell.

    update_width bool False

    Whether to resize the column width to accommodate for the new cell content.

    "},{"location":"widgets/data_table/#textual.widgets.data_table","title":"textual.widgets.data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.CellType","title":"CellType module-attribute","text":"
    CellType = TypeVar('CellType')\n

    Type used for cells in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CursorType","title":"CursorType module-attribute","text":"
    CursorType = Literal['cell', 'row', 'column', 'none']\n

    The valid types of cursors for DataTable.cursor_type.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CellDoesNotExist","title":"CellDoesNotExist class","text":"

    Bases: Exception

    The cell key/index was invalid.

    Raised when the coordinates or cell key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.CellKey","title":"CellKey class","text":"

    Bases: NamedTuple

    A unique identifier for a cell in the DataTable.

    A cell key is a (row_key, column_key) tuple.

    Even if the cell changes visual location (i.e. moves to a different coordinate in the table), this key can still be used to retrieve it, regardless of where it currently is.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.CellKey.column_key","title":"column_key instance-attribute","text":"
    column_key: ColumnKey\n

    The key of this cell's column.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.CellKey.row_key","title":"row_key instance-attribute","text":"
    row_key: RowKey\n

    The key of this cell's row.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column class","text":"

    Metadata for a column in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets._data_table.Column.get_render_width","title":"get_render_width method","text":"
    def get_render_width(self, data_table):\n

    Width, in cells, required to render the column with padding included.

    Parameters Parameter Default Description data_table DataTable[Any] required

    The data table where the column will be rendered.

    Returns Type Description int

    The width, in cells, required to render the column with padding included.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist class","text":"

    Bases: Exception

    Raised when the column index or column key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnKey","title":"ColumnKey class","text":"

    Bases: StringKey

    Uniquely identifies a column in the DataTable.

    Even if the visual location of the column changes due to sorting or other modifications, a key will always refer to the same column.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.DuplicateKey","title":"DuplicateKey class","text":"

    Bases: Exception

    The key supplied already exists.

    Raised when the RowKey or ColumnKey provided already refers to an existing row or column in the DataTable. Keys must be unique.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Row","title":"Row class","text":"

    Metadata for a row in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist class","text":"

    Bases: Exception

    Raised when the row index or row key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowKey","title":"RowKey class","text":"

    Bases: StringKey

    Uniquely identifies a row in the DataTable.

    Even if the visual location of the row changes due to sorting or other modifications, a key will always refer to the same row.

    "},{"location":"widgets/digits/","title":"Digits","text":"

    Added in version 0.33.0

    A widget to display numerical values in tall multi-line characters.

    The digits 0-9 are supported, in addition to the following characters +, -, ^, :, and \u00d7. Other characters will be displayed in a regular size font.

    You can set the text to be displayed in the constructor, or call update() to change the text after the widget has been mounted.

    This widget will respect the text-align rule.

    • Focusable
    • Container
    "},{"location":"widgets/digits/#example","title":"Example","text":"

    The following example displays a few digits of Pi:

    Outputdigits.py

    DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

    from textual.app import App, ComposeResult\nfrom textual.widgets import Digits\n\n\nclass DigitApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #pi {\n        border: double green;\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Digits(\"3.141,592,653,5897\", id=\"pi\")\n\n\nif __name__ == \"__main__\":\n    app = DigitApp()\n    app.run()\n

    Here's another example which uses Digits to display the current time:

    Outputclock.py

    ClockApp \u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u257a\u2501\u2513\u00a0\u00a0\u00a0\u00a0\u2513\u00a0\u250f\u2501\u2513 \u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2517\u2501\u252b\u00a0\u2501\u252b\u00a0:\u00a0\u00a0\u2503\u00a0\u2503\u00a0\u2503 \u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u257a\u2501\u251b\u00a0\u00a0\u00a0\u257a\u253b\u2578\u2517\u2501\u251b

    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Digits\n\n\nclass ClockApp(App):\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #clock {\n        width: auto;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Digits(\"\", id=\"clock\")\n\n    def on_ready(self) -> None:\n        self.update_clock()\n        self.set_interval(1, self.update_clock)\n\n    def update_clock(self) -> None:\n        clock = datetime.now().time()\n        self.query_one(Digits).update(f\"{clock:%T}\")\n\n\nif __name__ == \"__main__\":\n    app = ClockApp()\n    app.run()\n
    "},{"location":"widgets/digits/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/digits/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/digits/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/digits/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/digits/#textual.widgets.Digits","title":"textual.widgets.Digits class","text":"
    def __init__(\n    self,\n    value=\"\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A widget to display numerical values using a 3x3 grid of unicode characters.

    name: The name of the widget.\nid: The ID of the widget in the DOM.\nclasses: The CSS classes of the widget.\ndisabled: Whether the widget is disabled or not.\n
    "},{"location":"widgets/digits/#textual.widgets._digits.Digits.value","title":"value property","text":"
    value: str\n

    The current value displayed in the Digits.

    "},{"location":"widgets/digits/#textual.widgets._digits.Digits.update","title":"update method","text":"
    def update(self, value):\n

    Update the Digits with a new value.

    Parameters Parameter Default Description value str required

    New value to display.

    Raises Type Description ValueError

    If the value isn't a str.

    "},{"location":"widgets/directory_tree/","title":"DirectoryTree","text":"

    A tree control to navigate the contents of your filesystem.

    • Focusable
    • Container
    "},{"location":"widgets/directory_tree/#example","title":"Example","text":"

    The example below creates a simple tree to navigate the current working directory.

    from textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\n\n\nclass DirectoryTreeApp(App):\n    def compose(self) -> ComposeResult:\n        yield DirectoryTree(\"./\")\n\n\nif __name__ == \"__main__\":\n    app = DirectoryTreeApp()\n    app.run()\n
    "},{"location":"widgets/directory_tree/#filtering","title":"Filtering","text":"

    There may be times where you want to filter what appears in the DirectoryTree. To do this inherit from DirectoryTree and implement your own version of the filter_paths method. It should take an iterable of Python Path objects, and return those that pass the filter. For example, if you wanted to take the above code an filter out all of the \"hidden\" files and directories:

    Outputdirectory_tree_filtered.py

    DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CHANGELOG.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CODE_OF_CONDUCT.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CONTRIBUTING.md\u2584\u2584 \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0docs.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0faq.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0keys.log \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0LICENSE \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0Makefile \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-common.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-nav-offline.yml

    from pathlib import Path\nfrom typing import Iterable\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\n\n\nclass FilteredDirectoryTree(DirectoryTree):\n    def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:\n        return [path for path in paths if not path.name.startswith(\".\")]\n\n\nclass DirectoryTreeApp(App):\n    def compose(self) -> ComposeResult:\n        yield FilteredDirectoryTree(\"./\")\n\n\nif __name__ == \"__main__\":\n    app = DirectoryTreeApp()\n    app.run()\n
    "},{"location":"widgets/directory_tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/directory_tree/#messages","title":"Messages","text":"
    • DirectoryTree.FileSelected
    "},{"location":"widgets/directory_tree/#bindings","title":"Bindings","text":"

    The directory tree widget inherits the bindings from the tree widget.

    "},{"location":"widgets/directory_tree/#component-classes","title":"Component Classes","text":"

    The directory tree widget provides the following component classes:

    Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

    See also the component classes for Tree.

    "},{"location":"widgets/directory_tree/#see-also","title":"See Also","text":"
    • Tree code reference
    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree","title":"textual.widgets.DirectoryTree class","text":"
    def __init__(\n    self,\n    path,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Tree[DirEntry]

    A Tree widget that presents files and directories.

    Parameters Parameter Default Description path str | Path required

    Path to directory.

    name str | None None

    The name of the widget, or None for no name.

    id str | None None

    The ID of the widget in the DOM, or None for no ID.

    classes str | None None

    A space-separated list of classes, or None for no classes.

    disabled bool False

    Whether the directory tree is disabled or not.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"directory-tree--extension\",\n    \"directory-tree--file\",\n    \"directory-tree--folder\",\n    \"directory-tree--hidden\",\n}\n
    Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

    See also the component classes for Tree.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.PATH","title":"PATH class-attribute instance-attribute","text":"
    PATH: Callable[[str | Path], Path] = Path\n

    Callable that returns a fresh path object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.path","title":"path class-attribute instance-attribute","text":"
    path: var[str | Path] = path\n

    The path that is the root of the directory tree.

    Note

    This can be set to either a str or a pathlib.Path object, but the value will always be a pathlib.Path object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected","title":"DirectorySelected class","text":"
    def __init__(self, node, path):\n

    Bases: Message

    Posted when a directory is selected.

    Can be handled using on_directory_tree_directory_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The tree node for the directory that was selected.

    path Path required

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.control","title":"control property","text":"
    control: Tree[DirEntry]\n

    The Tree that had a directory selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
    node: TreeNode[DirEntry] = node\n

    The tree node of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
    path: Path = path\n

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected","title":"FileSelected class","text":"
    def __init__(self, node, path):\n

    Bases: Message

    Posted when a file is selected.

    Can be handled using on_directory_tree_file_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The tree node for the file that was selected.

    path Path required

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.control","title":"control property","text":"
    control: Tree[DirEntry]\n

    The Tree that had a file selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
    node: TreeNode[DirEntry] = node\n

    The tree node of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
    path: Path = path\n

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.clear_node","title":"clear_node method","text":"
    def clear_node(self, node):\n

    Clear all nodes under the given node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.filter_paths","title":"filter_paths method","text":"
    def filter_paths(self, paths):\n

    Filter the paths before adding them to the tree.

    Parameters Parameter Default Description paths Iterable[Path] required

    The paths to be filtered.

    Returns Type Description Iterable[Path]

    The filtered paths.

    By default this method returns all of the paths provided. To create a filtered DirectoryTree inherit from it and implement your own version of this method.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.process_label","title":"process_label method","text":"
    def process_label(self, label):\n

    Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered.

    Parameters Parameter Default Description label TextType required

    Label.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload","title":"reload method","text":"
    def reload(self):\n

    Reload the DirectoryTree contents.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload_node","title":"reload_node method","text":"
    def reload_node(self, node):\n

    Reload the given node's contents.

    The return value may be awaited to ensure the DirectoryTree has reached a stable state and is no longer performing any node reloading (of this node or any other nodes).

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The node to reload.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.render_label","title":"render_label method","text":"
    def render_label(self, node, base_style, style):\n

    Render a label for the given node.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    A tree node.

    base_style Style required

    The base style of the widget.

    style Style required

    The additional style for the label.

    Returns Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reset_node","title":"reset_node method","text":"
    def reset_node(self, node, label, data=None):\n

    Clear the subtree and reset the given node.

    Parameters Parameter Default Description node TreeNode[DirEntry] required

    The node to reset.

    label TextType required

    The label for the node.

    data DirEntry | None None

    Optional data for the node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.validate_path","title":"validate_path method","text":"
    def validate_path(self, path):\n

    Ensure that the path is of the Path type.

    Parameters Parameter Default Description path str | Path required

    The path to validate.

    Returns Type Description Path

    The validated Path value.

    Note

    The result will always be a Python Path object, regardless of the value given.

    "},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.watch_path","title":"watch_path async","text":"
    def watch_path(self):\n

    Watch for changes to the path of the directory tree.

    If the path is changed the directory tree will be repopulated using the new value as the root.

    "},{"location":"widgets/footer/","title":"Footer","text":"

    A simple footer widget which is docked to the bottom of its parent container. Displays available keybindings for the currently focused widget.

    • Focusable
    • Container
    "},{"location":"widgets/footer/#example","title":"Example","text":"

    The example below shows an app with a single keybinding that contains only a Footer widget. Notice how the Footer automatically displays the keybinding.

    Outputfooter.py

    FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

    from textual.app import App, ComposeResult\nfrom textual.binding import Binding\nfrom textual.widgets import Footer\n\n\nclass FooterApp(App):\n    BINDINGS = [\n        Binding(key=\"q\", action=\"quit\", description=\"Quit the app\"),\n        Binding(\n            key=\"question_mark\",\n            action=\"help\",\n            description=\"Show help screen\",\n            key_display=\"?\",\n        ),\n        Binding(key=\"delete\", action=\"delete\", description=\"Delete the thing\"),\n        Binding(key=\"j\", action=\"down\", description=\"Scroll down\", show=False),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = FooterApp()\n    app.run()\n
    "},{"location":"widgets/footer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight_key str None Stores the currently highlighted key. This is typically the key the cursor is hovered over in the footer."},{"location":"widgets/footer/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/footer/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/footer/#component-classes","title":"Component Classes","text":"

    The footer widget provides the following component classes:

    Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#additional-notes","title":"Additional Notes","text":"
    • You can prevent keybindings from appearing in the footer by setting the show argument of the Binding to False.
    • You can customize the text that appears for the key itself in the footer using the key_display argument of Binding.
    "},{"location":"widgets/footer/#textual.widgets.Footer","title":"textual.widgets.Footer class","text":"
    def __init__(self):\n

    Bases: Widget

    A simple footer widget which docks itself to the bottom of the parent container.

    "},{"location":"widgets/footer/#textual.widgets._footer.Footer.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"footer--description\",\n    \"footer--key\",\n    \"footer--highlight\",\n    \"footer--highlight-key\",\n}\n
    Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#textual.widgets._footer.Footer.watch_highlight_key","title":"watch_highlight_key async","text":"
    def watch_highlight_key(self):\n

    If highlight key changes we need to regenerate the text.

    "},{"location":"widgets/header/","title":"Header","text":"

    A simple header widget which docks itself to the top of the parent container.

    Note

    The application title which is shown in the header is taken from the title and sub_title of the application.

    • Focusable
    • Container
    "},{"location":"widgets/header/#example","title":"Example","text":"

    The example below shows an app with a Header.

    Outputheader.py

    HeaderApp \u2b58HeaderApp

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header\n\n\nclass HeaderApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n\n\nif __name__ == \"__main__\":\n    app = HeaderApp()\n    app.run()\n

    This example shows how to set the text in the Header using App.title and App.sub_title:

    Outputheader_app_title.py

    HeaderApp \u2b58Header\u00a0Application\u00a0\u2014\u00a0With\u00a0title\u00a0and\u00a0sub-title

    from textual.app import App, ComposeResult\nfrom textual.widgets import Header\n\n\nclass HeaderApp(App):\n    def compose(self) -> ComposeResult:\n        yield Header()\n\n    def on_mount(self) -> None:\n        self.title = \"Header Application\"\n        self.sub_title = \"With title and sub-title\"\n\n\nif __name__ == \"__main__\":\n    app = HeaderApp()\n    app.run()\n
    "},{"location":"widgets/header/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description tall bool True Whether the Header widget is displayed as tall or not. The tall variant is 3 cells tall by default. The non-tall variant is a single cell tall. This can be toggled by clicking on the header."},{"location":"widgets/header/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/header/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/header/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/header/#textual.widgets.Header","title":"textual.widgets.Header class","text":"
    def __init__(\n    self,\n    show_clock=False,\n    *,\n    name=None,\n    id=None,\n    classes=None\n):\n

    Bases: Widget

    A header widget with icon and clock.

    Parameters Parameter Default Description show_clock bool False

    True if the clock should be shown on the right of the header.

    name str | None None

    The name of the header widget.

    id str | None None

    The ID of the header widget in the DOM.

    classes str | None None

    The CSS classes of the header widget.

    "},{"location":"widgets/header/#textual.widgets._header.Header.screen_sub_title","title":"screen_sub_title property","text":"
    screen_sub_title: str\n

    The sub-title that this header will display.

    This depends on Screen.sub_title and App.sub_title.

    "},{"location":"widgets/header/#textual.widgets._header.Header.screen_title","title":"screen_title property","text":"
    screen_title: str\n

    The title that this header will display.

    This depends on Screen.title and App.title.

    "},{"location":"widgets/header/#textual.widgets._header.Header.tall","title":"tall class-attribute instance-attribute","text":"
    tall: Reactive[bool] = Reactive(False)\n

    Set to True for a taller header or False for a single line header.

    "},{"location":"widgets/input/","title":"Input","text":"

    A single-line text input widget.

    • Focusable
    • Container
    "},{"location":"widgets/input/#examples","title":"Examples","text":""},{"location":"widgets/input/#a-simple-example","title":"A Simple Example","text":"

    The example below shows how you might create a simple form using two Input widgets.

    Outputinput.py

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Input\n\n\nclass InputApp(App):\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"First Name\")\n        yield Input(placeholder=\"Last Name\")\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n
    "},{"location":"widgets/input/#input-types","title":"Input Types","text":"

    The Input widget supports a type parameter which will prevent the user from typing invalid characters. You can set type to any of the following values:

    input.type Description \"integer\" Restricts input to integers. \"number\" Restricts input to a floating point number. \"text\" Allow all text (no restrictions). Outputinput_types.py

    InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAn\u00a0integer\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aA\u00a0number\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Input\n\n\nclass InputApp(App):\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"An integer\", type=\"integer\")\n        yield Input(placeholder=\"A number\", type=\"number\")\n\n\nif __name__ == \"__main__\":\n    app = InputApp()\n    app.run()\n

    If you set type to something other than \"text\", then the Input will apply the appropriate validator.

    "},{"location":"widgets/input/#restricting-input","title":"Restricting Input","text":"

    You can limit input to particular characters by supplying the restrict parameter, which should be a regular expression. The Input widget will prevent the addition of any characters that would cause the regex to no longer match. For instance, if you wanted to limit characters to binary you could set restrict=r\"[01]*\".

    Note

    The restrict regular expression is applied to the full value and not just to the new character.

    "},{"location":"widgets/input/#maximum-length","title":"Maximum Length","text":"

    You can limit the length of the input by setting max_length to a value greater than zero. This will prevent the user from typing any more characters when the maximum has been reached.

    "},{"location":"widgets/input/#validating-input","title":"Validating Input","text":"

    You can supply one or more validators to the Input widget to validate the value.

    All the supplied validators will run when the value changes, the Input is submitted, or focus moves out of the Input. The values \"changed\", \"submitted\", and \"blur\", can be passed as an iterable to the Input parameter validate_on to request that validation occur only on the respective mesages. (See InputValidationOn and Input.validate_on.) For example, the code below creates an Input widget that only gets validated when the value is submitted explicitly:

    input = Input(validate_on=[\"submitted\"])\n

    Validation is considered to have failed if any of the validators fail.

    You can check whether the validation succeeded or failed inside an Input.Changed or Input.Submitted handler by looking at the validation_result attribute on these events.

    In the example below, we show how to combine multiple validators and update the UI to tell the user why validation failed. Click the tabs to see the output for validation failures and successes.

    input_validation.pyValidation FailureValidation Success
    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.validation import Function, Number, ValidationResult, Validator\nfrom textual.widgets import Input, Label, Pretty\n\n\nclass InputApp(App):\n    # (6)!\n    CSS = \"\"\"\n    Input.-valid {\n        border: tall $success 60%;\n    }\n    Input.-valid:focus {\n        border: tall $success;\n    }\n    Input {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    Pretty {\n        margin: 1 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Enter an even number between 1 and 100 that is also a palindrome.\")\n        yield Input(\n            placeholder=\"Enter a number...\",\n            validators=[\n                Number(minimum=1, maximum=100),  # (1)!\n                Function(is_even, \"Value is not even.\"),  # (2)!\n                Palindrome(),  # (3)!\n            ],\n        )\n        yield Pretty([])\n\n    @on(Input.Changed)\n    def show_invalid_reasons(self, event: Input.Changed) -> None:\n        # Updating the UI to show the reasons why validation failed\n        if not event.validation_result.is_valid:  # (4)!\n            self.query_one(Pretty).update(event.validation_result.failure_descriptions)\n        else:\n            self.query_one(Pretty).update([])\n\n\ndef is_even(value: str) -> bool:\n    try:\n        return int(value) % 2 == 0\n    except ValueError:\n        return False\n\n\n# A custom validator\nclass Palindrome(Validator):  # (5)!\n    def validate(self, value: str) -> ValidationResult:\n        \"\"\"Check a string is equal to its reverse.\"\"\"\n        if self.is_palindrome(value):\n            return self.success()\n        else:\n            return self.failure(\"That's not a palindrome :/\")\n\n    @staticmethod\n    def is_palindrome(value: str) -> bool:\n        return value == value[::-1]\n\n\napp = InputApp()\n\nif __name__ == \"__main__\":\n    app.run()\n
    1. Number is a built-in Validator. It checks that the value in the Input is a valid number, and optionally can check that it falls within a range.
    2. Function lets you quickly define custom validation constraints. In this case, we check the value in the Input is even.
    3. Palindrome is a custom Validator defined below.
    4. The Input.Changed event has a validation_result attribute which contains information about the validation that occurred when the value changed.
    5. Here's how we can implement a custom validator which checks if a string is a palindrome. Note how the description passed into self.failure corresponds to the message seen on UI.
    6. Textual offers default styling for the -invalid CSS class (a red border), which is automatically applied to Input when validation fails. We can also provide custom styling for the -valid class, as seen here. In this case, we add a green border around the Input to indicate successful validation.

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a-23\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e [ 'Must\u00a0be\u00a0between\u00a01\u00a0and\u00a0100.', 'Value\u00a0is\u00a0not\u00a0even.', \"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\" ]

    InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a44\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e []

    Textual offers several built-in validators for common requirements, but you can easily roll your own by extending Validator, as seen for Palindrome in the example above.

    "},{"location":"widgets/input/#validate-empty","title":"Validate Empty","text":"

    If you set valid_empty=True then empty values will bypass any validators, and empty values will be considered valid.

    "},{"location":"widgets/input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description cursor_blink bool True True if cursor blinking is enabled. value str \"\" The value currently in the text input. cursor_position int 0 The index of the cursor in the value string. placeholder str \"\" The dimmed placeholder text to display when the input is empty. password bool False True if the input should be masked. restrict str None Optional regular expression to restrict input. type str \"text\" The type of the input. max_length int None Maximum length of the input value. valid_empty bool False Allow empty values to bypass validation."},{"location":"widgets/input/#messages","title":"Messages","text":"
    • Input.Changed
    • Input.Submitted
    "},{"location":"widgets/input/#bindings","title":"Bindings","text":"

    The input widget defines the following bindings:

    Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#component-classes","title":"Component Classes","text":"

    The input widget provides the following component classes:

    Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#additional-notes","title":"Additional Notes","text":"
    • The spacing around the text content is due to border. To remove it, set border: none; in your CSS.
    "},{"location":"widgets/input/#textual.widgets.Input","title":"textual.widgets.Input class","text":"
    def __init__(\n    self,\n    value=None,\n    placeholder=\"\",\n    highlighter=None,\n    password=False,\n    *,\n    restrict=None,\n    type=\"text\",\n    max_length=0,\n    suggester=None,\n    validators=None,\n    validate_on=None,\n    valid_empty=False,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A text input widget.

    Parameters Parameter Default Description value str | None None

    An optional default value for the input.

    placeholder str ''

    Optional placeholder text for the input.

    highlighter Highlighter | None None

    An optional highlighter for the input.

    password bool False

    Flag to say if the field should obfuscate its content.

    restrict str | None None

    A regex to restrict character inputs.

    type InputType 'text'

    The type of the input.

    max_length int 0

    The maximum length of the input, or 0 for no maximum length.

    suggester Suggester | None None

    Suggester associated with this input instance.

    validators Validator | Iterable[Validator] | None None

    An iterable of validators that the Input value will be checked against.

    validate_on Iterable[InputValidationOn] | None None

    Zero or more of the values \"blur\", \"changed\", and \"submitted\", which determine when to do input validation. The default is to do validation for all messages.

    valid_empty bool False

    Empty values are valid.

    name str | None None

    Optional name for the input widget.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the input is disabled or not.

    "},{"location":"widgets/input/#textual.widgets._input.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"left\", \"cursor_left\", \"cursor left\", show=False\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_left_word\",\n        \"cursor left word\",\n        show=False,\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"cursor right\", show=False\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_right_word\",\n        \"cursor right word\",\n        show=False,\n    ),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"delete left\",\n        show=False,\n    ),\n    Binding(\"home,ctrl+a\", \"home\", \"home\", show=False),\n    Binding(\"end,ctrl+e\", \"end\", \"end\", show=False),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"delete right\",\n        show=False,\n    ),\n    Binding(\"enter\", \"submit\", \"submit\", show=False),\n    Binding(\n        \"ctrl+w\",\n        \"delete_left_word\",\n        \"delete left to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+u\",\n        \"delete_left_all\",\n        \"delete all to the left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+f\",\n        \"delete_right_word\",\n        \"delete right to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+k\",\n        \"delete_right_all\",\n        \"delete all to the right\",\n        show=False,\n    ),\n]\n
    Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#textual.widgets._input.Input.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"input--cursor\",\n    \"input--placeholder\",\n    \"input--suggestion\",\n}\n
    Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#textual.widgets._input.Input.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset: Offset\n

    The offset of the cursor of this input in screen-space. (x, y)/(column, row)

    "},{"location":"widgets/input/#textual.widgets._input.Input.cursor_width","title":"cursor_width property","text":"
    cursor_width: int\n

    The width of the input (with extra space for cursor at the end).

    "},{"location":"widgets/input/#textual.widgets._input.Input.is_valid","title":"is_valid property","text":"
    is_valid: bool\n

    Check if the value has passed validation.

    "},{"location":"widgets/input/#textual.widgets._input.Input.max_length","title":"max_length class-attribute instance-attribute","text":"
    max_length = max_length\n

    The maximum length of the input, in characters.

    "},{"location":"widgets/input/#textual.widgets._input.Input.restrict","title":"restrict class-attribute instance-attribute","text":"
    restrict = restrict\n

    A regular expression to limit changes in value.

    "},{"location":"widgets/input/#textual.widgets._input.Input.suggester","title":"suggester instance-attribute","text":"
    suggester: Suggester | None = suggester\n

    The suggester used to provide completions as the user types.

    "},{"location":"widgets/input/#textual.widgets._input.Input.type","title":"type class-attribute instance-attribute","text":"
    type = type\n

    The type of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.valid_empty","title":"valid_empty class-attribute instance-attribute","text":"
    valid_empty = var(False)\n

    Empty values should pass validation.

    "},{"location":"widgets/input/#textual.widgets._input.Input.validate_on","title":"validate_on instance-attribute","text":"
    validate_on = (\n    set(validate_on) & _POSSIBLE_VALIDATE_ON_VALUES\n    if validate_on is not None\n    else _POSSIBLE_VALIDATE_ON_VALUES\n)\n

    Set with event names to do input validation on.

    Validation can only be performed on blur, on input changes and on input submission.

    Example

    This creates an Input widget that only gets validated when the value is submitted explicitly:

    input = Input(validate_on=[\"submitted\"])\n
    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed","title":"Changed class","text":"

    Bases: Message

    Posted when the value changes.

    Can be handled using on_input_changed in a subclass of Input or in a parent widget in the DOM.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.control","title":"control property","text":"
    control: Input\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.input","title":"input instance-attribute","text":"
    input: Input\n

    The Input widget that was changed.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result: ValidationResult | None = None\n

    The result of validating the value (formed by combining the results from each validator), or None if validation was not performed (for example when no validators are specified in the Inputs init)

    "},{"location":"widgets/input/#textual.widgets._input.Input.Changed.value","title":"value instance-attribute","text":"
    value: str\n

    The value that the input was changed to.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted","title":"Submitted class","text":"

    Bases: Message

    Posted when the enter key is pressed within an Input.

    Can be handled using on_input_submitted in a subclass of Input or in a parent widget in the DOM.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.control","title":"control property","text":"
    control: Input\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.input","title":"input instance-attribute","text":"
    input: Input\n

    The Input widget that is being submitted.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result: ValidationResult | None = None\n

    The result of validating the value on submission, formed by combining the results for each validator. This value will be None if no validation was performed, which will be the case if no validators are supplied to the corresponding Input widget.

    "},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.value","title":"value instance-attribute","text":"
    value: str\n

    The value of the Input being submitted.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left","title":"action_cursor_left method","text":"
    def action_cursor_left(self):\n

    Move the cursor one position to the left.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left_word","title":"action_cursor_left_word method","text":"
    def action_cursor_left_word(self):\n

    Move the cursor left to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right","title":"action_cursor_right method","text":"
    def action_cursor_right(self):\n

    Accept an auto-completion or move the cursor one position to the right.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right_word","title":"action_cursor_right_word method","text":"
    def action_cursor_right_word(self):\n

    Move the cursor right to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left","title":"action_delete_left method","text":"
    def action_delete_left(self):\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_all","title":"action_delete_left_all method","text":"
    def action_delete_left_all(self):\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_word","title":"action_delete_left_word method","text":"
    def action_delete_left_word(self):\n

    Delete leftward of the cursor position to the start of a word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right","title":"action_delete_right method","text":"
    def action_delete_right(self):\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_all","title":"action_delete_right_all method","text":"
    def action_delete_right_all(self):\n

    Delete the current character and all characters to the right of the cursor position.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_word","title":"action_delete_right_word method","text":"
    def action_delete_right_word(self):\n

    Delete the current character and all rightward to the start of the next word.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_end","title":"action_end method","text":"
    def action_end(self):\n

    Move the cursor to the end of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_home","title":"action_home method","text":"
    def action_home(self):\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.action_submit","title":"action_submit async","text":"
    def action_submit(self):\n

    Handle a submit action.

    Normally triggered by the user pressing Enter. This may also run any validators.

    "},{"location":"widgets/input/#textual.widgets._input.Input.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the input.

    "},{"location":"widgets/input/#textual.widgets._input.Input.insert_text_at_cursor","title":"insert_text_at_cursor method","text":"
    def insert_text_at_cursor(self, text):\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters Parameter Default Description text str required

    New text to insert.

    "},{"location":"widgets/input/#textual.widgets._input.Input.restricted","title":"restricted method","text":"
    def restricted(self):\n

    Called when a character has been restricted.

    The default behavior is to play the system bell. You may want to override this method if you want to disable the bell or do something else entirely.

    "},{"location":"widgets/input/#textual.widgets._input.Input.validate","title":"validate method","text":"
    def validate(self, value):\n

    Run all the validators associated with this Input on the supplied value.

    Runs all validators, combines the result into one. If any of the validators failed, the combined result will be a failure. If no validators are present, None will be returned. This also sets the -invalid CSS class on the Input if the validation fails, and sets the -valid CSS class on the Input if the validation succeeds.

    Returns Type Description ValidationResult | None

    A ValidationResult indicating whether all validators succeeded or not. That is, if any validator fails, the result will be an unsuccessful validation.

    "},{"location":"widgets/label/","title":"Label","text":"

    Added in version 0.5.0

    A widget which displays static text, but which can also contain more complex Rich renderables.

    • Focusable
    • Container
    "},{"location":"widgets/label/#example","title":"Example","text":"

    The example below shows how you can use a Label widget to display some text.

    Outputlabel.py

    LabelApp Hello,\u00a0world!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label\n\n\nclass LabelApp(App):\n    def compose(self) -> ComposeResult:\n        yield Label(\"Hello, world!\")\n\n\nif __name__ == \"__main__\":\n    app = LabelApp()\n    app.run()\n
    "},{"location":"widgets/label/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/label/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/label/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/label/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/label/#textual.widgets.Label","title":"textual.widgets.Label class","text":"

    Bases: Static

    A simple label widget for displaying text-oriented renderables.

    "},{"location":"widgets/list_item/","title":"ListItem","text":"

    Added in version 0.6.0

    ListItem is the type of the elements in a ListView.

    • Focusable
    • Container
    "},{"location":"widgets/list_item/#example","title":"Example","text":"

    The example below shows an app with a simple ListView, consisting of multiple ListItems. The arrow keys can be used to navigate the list.

    Outputlist_view.py

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    "},{"location":"widgets/list_item/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted bool False True if this ListItem is highlighted"},{"location":"widgets/list_item/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/list_item/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/list_item/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/list_item/#textual.widgets.ListItem","title":"textual.widgets.ListItem class","text":"

    Bases: Widget

    A widget that is an item within a ListView.

    A ListItem is designed for use within a ListView, please see ListView's documentation for more details on use.

    "},{"location":"widgets/list_item/#textual.widgets._list_item.ListItem.highlighted","title":"highlighted class-attribute instance-attribute","text":"
    highlighted = reactive(False)\n

    Is this item highlighted?

    "},{"location":"widgets/list_view/","title":"ListView","text":"

    Added in version 0.6.0

    Displays a vertical list of ListItems which can be highlighted and selected. Supports keyboard navigation.

    • Focusable
    • Container
    "},{"location":"widgets/list_view/#example","title":"Example","text":"

    The example below shows an app with a simple ListView.

    Outputlist_view.pylist_view.tcss

    ListViewExample One Two Three

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\n\n\nclass ListViewExample(App):\n    CSS_PATH = \"list_view.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = ListViewExample()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nListView {\n    width: 30;\n    height: auto;\n    margin: 2 2;\n}\n\nLabel {\n    padding: 1 2;\n}\n
    "},{"location":"widgets/list_view/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description index int 0 The currently highlighted index."},{"location":"widgets/list_view/#messages","title":"Messages","text":"
    • ListView.Highlighted
    • ListView.Selected
    "},{"location":"widgets/list_view/#bindings","title":"Bindings","text":"

    The list view widget defines the following bindings:

    Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/list_view/#textual.widgets.ListView","title":"textual.widgets.ListView class","text":"
    def __init__(\n    self,\n    *children,\n    initial_index=0,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: VerticalScroll

    A vertical list view widget.

    Displays a vertical list of ListItems which can be highlighted and selected using the mouse or keyboard.

    Attributes Name Type Description index

    The index in the list that's currently highlighted.

    Parameters Parameter Default Description *children ListItem ()

    The ListItems to display in the list.

    initial_index int | None 0

    The index that should be highlighted when the list is first mounted.

    name str | None None

    The name of the widget.

    id str | None None

    The unique ID of the widget used in CSS/query selection.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the ListView is disabled or not.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.highlighted_child","title":"highlighted_child property","text":"
    highlighted_child: ListItem | None\n

    The currently highlighted ListItem, or None if nothing is highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.index","title":"index class-attribute instance-attribute","text":"
    index = reactive[Optional[int]](0, always_update=True)\n

    The index of the currently highlighted item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted","title":"Highlighted class","text":"
    def __init__(self, list_view, item):\n

    Bases: Message

    Posted when the highlighted item changes.

    Highlighted item is controlled using up/down keys. Can be handled using on_list_view_highlighted in a subclass of ListView or in a parent widget in the DOM.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'item'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.control","title":"control property","text":"
    control: ListView\n

    The view that contains the item highlighted.

    This is an alias for Highlighted.list_view and is used by the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.item","title":"item instance-attribute","text":"
    item: ListItem | None = item\n

    The highlighted item, if there is one highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
    list_view: ListView = list_view\n

    The view that contains the item highlighted.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected","title":"Selected class","text":"
    def __init__(self, list_view, item):\n

    Bases: Message

    Posted when a list item is selected, e.g. when you press the enter key on it.

    Can be handled using on_list_view_selected in a subclass of ListView or in a parent widget in the DOM.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'item'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.control","title":"control property","text":"
    control: ListView\n

    The view that contains the item selected.

    This is an alias for Selected.list_view and is used by the on decorator.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.item","title":"item instance-attribute","text":"
    item: ListItem = item\n

    The selected item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
    list_view: ListView = list_view\n

    The view that contains the item selected.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Highlight the next item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Highlight the previous item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_select_cursor","title":"action_select_cursor method","text":"
    def action_select_cursor(self):\n

    Select the current item in the list.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.append","title":"append method","text":"
    def append(self, item):\n

    Append a new ListItem to the end of the ListView.

    Parameters Parameter Default Description item ListItem required

    The ListItem to append.

    Returns Type Description AwaitMount

    An awaitable that yields control to the event loop until the DOM has been updated with the new child item.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all items from the ListView.

    Returns Type Description AwaitRemove

    An awaitable that yields control to the event loop until the DOM has been updated to reflect all children being removed.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.extend","title":"extend method","text":"
    def extend(self, items):\n

    Append multiple new ListItems to the end of the ListView.

    Parameters Parameter Default Description items Iterable[ListItem] required

    The ListItems to append.

    Returns Type Description AwaitMount

    An awaitable that yields control to the event loop until the DOM has been updated with the new child items.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.validate_index","title":"validate_index method","text":"
    def validate_index(self, index):\n

    Clamp the index to the valid range, or set to None if there's nothing to highlight.

    Parameters Parameter Default Description index int | None required

    The index to clamp.

    Returns Type Description int | None

    The clamped index.

    "},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.watch_index","title":"watch_index method","text":"
    def watch_index(self, old_index, new_index):\n

    Updates the highlighting when the index changes.

    "},{"location":"widgets/loading_indicator/","title":"LoadingIndicator","text":"

    Added in version 0.15.0

    Displays pulsating dots to indicate when data is being loaded.

    • Focusable
    • Container
    "},{"location":"widgets/loading_indicator/#example","title":"Example","text":"

    Simple usage example:

    Outputloading_indicator.py

    LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

    from textual.app import App, ComposeResult\nfrom textual.widgets import LoadingIndicator\n\n\nclass LoadingApp(App):\n    def compose(self) -> ComposeResult:\n        yield LoadingIndicator()\n\n\nif __name__ == \"__main__\":\n    app = LoadingApp()\n    app.run()\n
    "},{"location":"widgets/loading_indicator/#changing-indicator-color","title":"Changing Indicator Color","text":"

    You can set the color of the loading indicator by setting its color style.

    Here's how you would do that with CSS:

    LoadingIndicator {\n    color: red;\n}\n
    "},{"location":"widgets/loading_indicator/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/loading_indicator/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/loading_indicator/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/loading_indicator/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator","title":"textual.widgets.LoadingIndicator class","text":"
    def __init__(\n    self, name=None, id=None, classes=None, disabled=False\n):\n

    Bases: Widget

    Display an animated loading indicator.

    Parameters Parameter Default Description name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/log/","title":"Log","text":"

    Added in version 0.32.0

    A Log widget displays lines of text which may be appended to in realtime.

    Call Log.write_line to write a line at a time, or Log.write_lines to write multiple lines at once. Call Log.clear to clear the Log widget.

    Tip

    See also RichLog which can write more than just text, and supports a number of advanced features.

    • Focusable
    • Container
    "},{"location":"widgets/log/#example","title":"Example","text":"

    The example below shows how to write text to a Log widget:

    Outputlog.py

    LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

    from textual.app import App, ComposeResult\nfrom textual.widgets import Log\n\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\n\n\nclass LogApp(App):\n    \"\"\"An app with a simple log.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Log()\n\n    def on_ready(self) -> None:\n        log = self.query_one(Log)\n        log.write_line(\"Hello, World!\")\n        for _ in range(10):\n            log.write_line(TEXT)\n\n\nif __name__ == \"__main__\":\n    app = LogApp()\n    app.run()\n
    "},{"location":"widgets/log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description max_lines int None Maximum number of lines in the log or None for no maximum. auto_scroll bool False Scroll to end of log when new lines are added."},{"location":"widgets/log/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/log/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/log/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/log/#textual.widgets.Log","title":"textual.widgets.Log class","text":"
    def __init__(\n    self,\n    highlight=False,\n    max_lines=None,\n    auto_scroll=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Bases: ScrollView

    A widget to log text.

    Parameters Parameter Default Description highlight bool False

    Enable highlighting.

    max_lines int | None None

    Maximum number of lines to display.

    auto_scroll bool True

    Scroll to end on new lines.

    name str | None None

    The name of the text log.

    id str | None None

    The ID of the text log in the DOM.

    classes str | None None

    The CSS classes of the text log.

    disabled bool False

    Whether the text log is disabled or not.

    "},{"location":"widgets/log/#textual.widgets._log.Log.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll: var[bool] = auto_scroll\n

    Automatically scroll to new lines.

    "},{"location":"widgets/log/#textual.widgets._log.Log.highlight","title":"highlight instance-attribute","text":"
    highlight = highlight\n

    Enable highlighting.

    "},{"location":"widgets/log/#textual.widgets._log.Log.highlighter","title":"highlighter instance-attribute","text":"
    highlighter = ReprHighlighter()\n

    The Rich Highlighter object to use, if highlight=True

    "},{"location":"widgets/log/#textual.widgets._log.Log.line_count","title":"line_count property","text":"
    line_count: int\n

    Number of lines of content.

    "},{"location":"widgets/log/#textual.widgets._log.Log.lines","title":"lines property","text":"
    lines: Sequence[str]\n

    The raw lines in the Log.

    Note that this attribute is read only. Changing the lines will not update the Log's contents.

    "},{"location":"widgets/log/#textual.widgets._log.Log.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines: var[int | None] = max_lines\n

    Maximum number of lines to show

    "},{"location":"widgets/log/#textual.widgets._log.Log.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the Log.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.notify_style_update","title":"notify_style_update method","text":"
    def notify_style_update(self):\n

    Called by Textual when styles update.

    "},{"location":"widgets/log/#textual.widgets._log.Log.refresh_lines","title":"refresh_lines method","text":"
    def refresh_lines(self, y_start, line_count=1):\n

    Refresh one or more lines.

    Parameters Parameter Default Description y_start int required

    First line to refresh.

    line_count int 1

    Total number of lines to refresh.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write","title":"write method","text":"
    def write(self, data, scroll_end=None):\n

    Write to the log.

    Parameters Parameter Default Description data str required

    Data to write.

    scroll_end bool | None None

    Scroll to the end after writing, or None to use self.auto_scroll.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write_line","title":"write_line method","text":"
    def write_line(self, line):\n

    Write content on a new line.

    Parameters Parameter Default Description line str required

    String to write to the log.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets._log.Log.write_lines","title":"write_lines method","text":"
    def write_lines(self, lines, scroll_end=None):\n

    Write an iterable of lines.

    Parameters Parameter Default Description lines Iterable[str] required

    An iterable of strings to write.

    scroll_end bool | None None

    Scroll to the end after writing, or None to use self.auto_scroll.

    Returns Type Description Self

    The Log instance.

    "},{"location":"widgets/markdown/","title":"Markdown","text":"

    Added in version 0.11.0

    A widget to display a Markdown document.

    • Focusable
    • Container

    Tip

    See MarkdownViewer for a widget that adds additional features such as a Table of Contents.

    "},{"location":"widgets/markdown/#example","title":"Example","text":"

    The following example displays Markdown from a string.

    Outputmarkdown.py

    MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Markdown\n\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Document\n\nThis is an example of Textual's `Markdown` widget.\n\n## Features\n\nMarkdown syntax and extensions are supported.\n\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\"\"\"\n\n\nclass MarkdownExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield Markdown(EXAMPLE_MARKDOWN)\n\n\nif __name__ == \"__main__\":\n    app = MarkdownExampleApp()\n    app.run()\n
    "},{"location":"widgets/markdown/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/markdown/#messages","title":"Messages","text":"
    • Markdown.TableOfContentsUpdated
    • Markdown.TableOfContentsSelected
    • Markdown.LinkClicked
    "},{"location":"widgets/markdown/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/markdown/#component-classes","title":"Component Classes","text":"

    The markdown widget provides the following component classes:

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#see-also","title":"See Also","text":"
    • MarkdownViewer code reference
    "},{"location":"widgets/markdown/#textual.widgets.Markdown","title":"textual.widgets.Markdown class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: Widget

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown or None to leave blank for now.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute instance-attribute","text":"
    COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
    def __init__(self, markdown, href):\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget containing the link clicked.

    This is an alias for LinkClicked.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
    href: str = href\n

    The link that was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
    def __init__(self, markdown, block_id):\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id: str = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget where the selected item is.

    This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
    def __init__(self, markdown, table_of_contents):\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget associated with the table of contents.

    This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents: TableOfContentsType = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
    def goto_anchor(self, anchor):\n

    Try and find the given anchor in the current document.

    Parameters Parameter Default Description anchor str required

    The anchor to try and find.

    Note

    The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

    Note that the slugging method used is similar to that found on GitHub.

    Returns Type Description bool

    True when the anchor was found in the current document, False otherwise.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
    def load(self, path):\n

    Load a new Markdown document.

    Parameters Parameter Default Description path Path required

    Path to the document.

    Raises Type Description OSError

    If there was some form of error loading the document.

    Note

    The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    def sanitize_location(location):\n

    Given a location, break out the path and any anchor.

    Parameters Parameter Default Description location str required

    The location to sanitize.

    Returns Type Description Path

    A tuple of the path to the location cleaned of any anchor, plus

    str

    the anchor (or an empty string if none was found).

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
    def unhandled_token(self, token):\n

    Process an unhandled token.

    Parameters Parameter Default Description token Token required

    The MarkdownIt token to handle.

    Returns Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
    def update(self, markdown):\n

    Update the document with new Markdown.

    Parameters Parameter Default Description markdown str required

    A string containing Markdown.

    Returns Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown_viewer/","title":"MarkdownViewer","text":"

    Added in version 0.11.0

    A Widget to display Markdown content with an optional Table of Contents.

    • Focusable
    • Container

    Note

    This Widget adds browser-like functionality on top of the Markdown widget.

    "},{"location":"widgets/markdown_viewer/#example","title":"Example","text":"

    The following example displays Markdown from a string and a Table of Contents.

    Outputmarkdown.py

    MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

    from textual.app import App, ComposeResult\nfrom textual.widgets import MarkdownViewer\n\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Viewer\n\nThis is an example of Textual's `MarkdownViewer` widget.\n\n\n## Features\n\nMarkdown syntax and extensions are supported.\n\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\n## Tables\n\nTables are displayed in a DataTable widget.\n\n| Name            | Type   | Default | Description                        |\n| --------------- | ------ | ------- | ---------------------------------- |\n| `show_header`   | `bool` | `True`  | Show the table header              |\n| `fixed_rows`    | `int`  | `0`     | Number of fixed rows               |\n| `fixed_columns` | `int`  | `0`     | Number of fixed columns            |\n| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |\n| `header_height` | `int`  | `1`     | Height of header row               |\n| `show_cursor`   | `bool` | `True`  | Show a cell cursor                 |\n\n\n## Code Blocks\n\nCode blocks are syntax highlighted, with guidelines.\n\n```python\nclass ListViewExample(App):\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n```\n\"\"\"\n\n\nclass MarkdownExampleApp(App):\n    def compose(self) -> ComposeResult:\n        yield MarkdownViewer(EXAMPLE_MARKDOWN, show_table_of_contents=True)\n\n\nif __name__ == \"__main__\":\n    app = MarkdownExampleApp()\n    app.run()\n
    "},{"location":"widgets/markdown_viewer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_table_of_contents bool True Wether a Table of Contents should be displayed with the Markdown."},{"location":"widgets/markdown_viewer/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/markdown_viewer/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/markdown_viewer/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/markdown_viewer/#see-also","title":"See Also","text":"
    • Markdown code reference
    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer","title":"textual.widgets.MarkdownViewer class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    show_table_of_contents=True,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: VerticalScroll

    A Markdown viewer widget.

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown, or None to leave blank.

    show_table_of_contents bool True

    Show a table of contents in a sidebar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.document","title":"document property","text":"
    document: Markdown\n

    The Markdown document widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
    table_of_contents: MarkdownTableOfContents\n

    The table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.back","title":"back async","text":"
    def back(self):\n

    Go back one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.forward","title":"forward async","text":"
    def forward(self):\n

    Go forward one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.go","title":"go async","text":"
    def go(self, location):\n

    Navigate to a new document path.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown","title":"textual.widgets.markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.TableOfContentsType","title":"TableOfContentsType module-attribute","text":"
    TableOfContentsType: TypeAlias = (\n    \"list[tuple[int, str, str | None]]\"\n)\n

    Information about the table of contents of a markdown document.

    The triples encode the level, the label, and the optional block id of each heading.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown","title":"Markdown class","text":"
    def __init__(\n    self,\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n):\n

    Bases: Widget

    Parameters Parameter Default Description markdown str | None None

    String containing Markdown or None to leave blank for now.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    parser_factory Callable[[], MarkdownIt] | None None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute instance-attribute","text":"
    COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

    These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

    Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
    def __init__(self, markdown, href):\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget containing the link clicked.

    This is an alias for LinkClicked.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
    href: str = href\n

    The link that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
    def __init__(self, markdown, block_id):\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id: str = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget where the selected item is.

    This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
    def __init__(self, markdown, table_of_contents):\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control: Markdown\n

    The Markdown widget associated with the table of contents.

    This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents: TableOfContentsType = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
    def goto_anchor(self, anchor):\n

    Try and find the given anchor in the current document.

    Parameters Parameter Default Description anchor str required

    The anchor to try and find.

    Note

    The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

    Note that the slugging method used is similar to that found on GitHub.

    Returns Type Description bool

    True when the anchor was found in the current document, False otherwise.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
    def load(self, path):\n

    Load a new Markdown document.

    Parameters Parameter Default Description path Path required

    Path to the document.

    Raises Type Description OSError

    If there was some form of error loading the document.

    Note

    The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    def sanitize_location(location):\n

    Given a location, break out the path and any anchor.

    Parameters Parameter Default Description location str required

    The location to sanitize.

    Returns Type Description Path

    A tuple of the path to the location cleaned of any anchor, plus

    str

    the anchor (or an empty string if none was found).

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
    def unhandled_token(self, token):\n

    Process an unhandled token.

    Parameters Parameter Default Description token Token required

    The MarkdownIt token to handle.

    Returns Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
    def update(self, markdown):\n

    Update the document with new Markdown.

    Parameters Parameter Default Description markdown str required

    A string containing Markdown.

    Returns Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock","title":"MarkdownBlock class","text":"
    def __init__(self, markdown, *args, **kwargs):\n

    Bases: Static

    The base class for a Markdown Element.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownBlock.action_link","title":"action_link async","text":"
    def action_link(self, href):\n

    Called on link click.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents","title":"MarkdownTableOfContents class","text":"
    def __init__(\n    self,\n    markdown,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Bases: Widget

    Displays a table of contents for a markdown document.

    Parameters Parameter Default Description markdown Markdown required

    The Markdown document associated with this table of contents.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.markdown","title":"markdown instance-attribute","text":"
    markdown: Markdown = markdown\n

    The Markdown document associated with this table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.table_of_contents","title":"table_of_contents class-attribute instance-attribute","text":"
    table_of_contents = reactive[Optional[TableOfContentsType]](\n    None, init=False\n)\n

    Underlying data to populate the table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.rebuild_table_of_contents","title":"rebuild_table_of_contents method","text":"
    def rebuild_table_of_contents(self, table_of_contents):\n

    Rebuilds the tree representation of the table of contents data.

    Parameters Parameter Default Description table_of_contents TableOfContentsType required

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownTableOfContents.watch_table_of_contents","title":"watch_table_of_contents method","text":"
    def watch_table_of_contents(self, table_of_contents):\n

    Triggered when the table of contents changes.

    "},{"location":"widgets/option_list/","title":"OptionList","text":"

    Added in version 0.17.0

    A widget for showing a vertical list of Rich renderable options.

    • Focusable
    • Container
    "},{"location":"widgets/option_list/#examples","title":"Examples","text":""},{"location":"widgets/option_list/#options-as-simple-strings","title":"Options as simple strings","text":"

    An OptionList can be constructed with a simple collection of string options:

    Outputoption_list_strings.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(\n            \"Aerilon\",\n            \"Aquaria\",\n            \"Canceron\",\n            \"Caprica\",\n            \"Gemenon\",\n            \"Leonis\",\n            \"Libran\",\n            \"Picon\",\n            \"Sagittaron\",\n            \"Scorpia\",\n            \"Tauron\",\n            \"Virgon\",\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#options-as-option-instances","title":"Options as Option instances","text":"

    For finer control over the options, the Option class can be used; this allows for setting IDs, setting initial disabled state, etc. The Separator class can be used to add separator lines between options.

    Outputoption_list_options.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u2581\u2581\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nfrom textual.widgets.option_list import Option, Separator\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(\n            Option(\"Aerilon\", id=\"aer\"),\n            Option(\"Aquaria\", id=\"aqu\"),\n            Separator(),\n            Option(\"Canceron\", id=\"can\"),\n            Option(\"Caprica\", id=\"cap\", disabled=True),\n            Separator(),\n            Option(\"Gemenon\", id=\"gem\"),\n            Separator(),\n            Option(\"Leonis\", id=\"leo\"),\n            Option(\"Libran\", id=\"lib\"),\n            Separator(),\n            Option(\"Picon\", id=\"pic\"),\n            Separator(),\n            Option(\"Sagittaron\", id=\"sag\"),\n            Option(\"Scorpia\", id=\"sco\"),\n            Separator(),\n            Option(\"Tauron\", id=\"tau\"),\n            Separator(),\n            Option(\"Virgon\", id=\"vir\"),\n        )\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#options-as-rich-renderables","title":"Options as Rich renderables","text":"

    Because the prompts for the options can be Rich renderables, this means they can be any height you wish. As an example, here is an option list comprised of Rich tables:

    Outputoption_list_tables.pyoption_list.tcss

    OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u2587\u2587\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from __future__ import annotations\n\nfrom rich.table import Table\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\n\nCOLONIES: tuple[tuple[str, str, str, str], ...] = (\n    (\"Aerilon\", \"Demeter\", \"1.2 Billion\", \"Gaoth\"),\n    (\"Aquaria\", \"Hermes\", \"75,000\", \"None\"),\n    (\"Canceron\", \"Hephaestus\", \"6.7 Billion\", \"Hades\"),\n    (\"Caprica\", \"Apollo\", \"4.9 Billion\", \"Caprica City\"),\n    (\"Gemenon\", \"Hera\", \"2.8 Billion\", \"Oranu\"),\n    (\"Leonis\", \"Artemis\", \"2.6 Billion\", \"Luminere\"),\n    (\"Libran\", \"Athena\", \"2.1 Billion\", \"None\"),\n    (\"Picon\", \"Poseidon\", \"1.4 Billion\", \"Queenstown\"),\n    (\"Sagittaron\", \"Zeus\", \"1.7 Billion\", \"Tawa\"),\n    (\"Scorpia\", \"Dionysus\", \"450 Million\", \"Celeste\"),\n    (\"Tauron\", \"Ares\", \"2.5 Billion\", \"Hypatia\"),\n    (\"Virgon\", \"Hestia\", \"4.3 Billion\", \"Boskirk\"),\n)\n\n\nclass OptionListApp(App[None]):\n    CSS_PATH = \"option_list.tcss\"\n\n    @staticmethod\n    def colony(name: str, god: str, population: str, capital: str) -> Table:\n        table = Table(title=f\"Data for {name}\", expand=True)\n        table.add_column(\"Patron God\")\n        table.add_column(\"Population\")\n        table.add_column(\"Capital City\")\n        table.add_row(god, population, capital)\n        return table\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield OptionList(*[self.colony(*row) for row in COLONIES])\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    OptionListApp().run()\n
    Screen {\n    align: center middle;\n}\n\nOptionList {\n    width: 70%;\n    height: 80%;\n}\n
    "},{"location":"widgets/option_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted option. None means nothing is highlighted."},{"location":"widgets/option_list/#messages","title":"Messages","text":"
    • OptionList.OptionHighlighted
    • OptionList.OptionSelected

    Both of the messages above inherit from the common base OptionList.OptionMessage, so refer to its documentation to see what attributes are available.

    "},{"location":"widgets/option_list/#bindings","title":"Bindings","text":"

    The option list widget defines the following bindings:

    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#component-classes","title":"Component Classes","text":"

    The option list provides the following component classes:

    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.OptionList","title":"textual.widgets.OptionList class","text":"
    def __init__(\n    self,\n    *content,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    wrap=True\n):\n

    Bases: ScrollView

    A vertical option list with bounce-bar highlighting.

    Parameters Parameter Default Description *content NewOptionListContent ()

    The content for the option list.

    name str | None None

    The name of the option list.

    id str | None None

    The ID of the option list in the DOM.

    classes str | None None

    The CSS classes of the option list.

    disabled bool False

    Whether the option list is disabled or not.

    wrap bool True

    Should prompts be auto-wrapped?

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"down\", \"cursor_down\", \"Down\", show=False),\n    Binding(\"end\", \"last\", \"Last\", show=False),\n    Binding(\"enter\", \"select\", \"Select\", show=False),\n    Binding(\"home\", \"first\", \"First\", show=False),\n    Binding(\n        \"pagedown\", \"page_down\", \"Page Down\", show=False\n    ),\n    Binding(\"pageup\", \"page_up\", \"Page Up\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Up\", show=False),\n]\n
    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"option-list--option\",\n    \"option-list--option-disabled\",\n    \"option-list--option-highlighted\",\n    \"option-list--option-highlighted-disabled\",\n    \"option-list--option-hover\",\n    \"option-list--option-hover-disabled\",\n    \"option-list--option-hover-highlighted\",\n    \"option-list--option-hover-highlighted-disabled\",\n    \"option-list--separator\",\n}\n
    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.highlighted","title":"highlighted class-attribute instance-attribute","text":"
    highlighted: reactive[int | None] = None\n

    The index of the currently-highlighted option, or None if no option is highlighted.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.option_count","title":"option_count property","text":"
    option_count: int\n

    The count of options.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionHighlighted","title":"OptionHighlighted class","text":"

    Bases: OptionMessage

    Message sent when an option is highlighted.

    Can be handled using on_option_list_option_highlighted in a subclass of OptionList or in a parent node in the DOM.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage","title":"OptionMessage class","text":"
    def __init__(self, option_list, index):\n

    Bases: Message

    Base class for all option messages.

    Parameters Parameter Default Description option_list OptionList required

    The option list that owns the option.

    index int required

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.control","title":"control property","text":"
    control: OptionList\n

    The option list that sent the message.

    This is an alias for OptionMessage.option_list and is used by the on decorator.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
    option: Option = option_list.get_option_at_index(index)\n

    The highlighted option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
    option_id: str | None = self.option.id\n

    The ID of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
    option_index: int = index\n

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
    option_list: OptionList = option_list\n

    The option list that sent the message.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionSelected","title":"OptionSelected class","text":"

    Bases: OptionMessage

    Message sent when an option is selected.

    Can be handled using on_option_list_option_selected in a subclass of OptionList or in a parent node in the DOM.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Move the highlight down by one option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Move the highlight up by one option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_first","title":"action_first method","text":"
    def action_first(self):\n

    Move the highlight to the first option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_last","title":"action_last method","text":"
    def action_last(self):\n

    Move the highlight to the last option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the highlight down one page.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the highlight up one page.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_select","title":"action_select method","text":"
    def action_select(self):\n

    Select the currently-highlighted option.

    If no option is selected, then nothing happens. If an option is selected, a OptionList.OptionSelected message will be posted.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_option","title":"add_option method","text":"
    def add_option(self, item=None):\n

    Add a new option to the end of the option list.

    Parameters Parameter Default Description item NewOptionListContent None

    The new item to add.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_options","title":"add_options method","text":"
    def add_options(self, items):\n

    Add new options to the end of the option list.

    Parameters Parameter Default Description items Iterable[NewOptionListContent] required

    The new items to add.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    Note

    All options are checked for duplicate IDs before any option is added. A duplicate ID will cause none of the passed items to be added to the option list.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.clear_options","title":"clear_options method","text":"
    def clear_options(self):\n

    Clear the content of the option list.

    Returns Type Description Self

    The OptionList instance.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option","title":"disable_option method","text":"
    def disable_option(self, option_id):\n

    Disable the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to disable.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option_at_index","title":"disable_option_at_index method","text":"
    def disable_option_at_index(self, index):\n

    Disable the option at the given index.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option","title":"enable_option method","text":"
    def enable_option(self, option_id):\n

    Enable the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to enable.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option_at_index","title":"enable_option_at_index method","text":"
    def enable_option_at_index(self, index):\n

    Enable the option at the given index.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option","title":"get_option method","text":"
    def get_option(self, option_id):\n

    Get the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to get.

    Returns Type Description Option

    The option with the ID.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_at_index","title":"get_option_at_index method","text":"
    def get_option_at_index(self, index):\n

    Get the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to get.

    Returns Type Description Option

    The option at that index.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_index","title":"get_option_index method","text":"
    def get_option_index(self, option_id):\n

    Get the index of the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to get the index of.

    Returns Type Description int

    The index of the item with the given ID.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option","title":"remove_option method","text":"
    def remove_option(self, option_id):\n

    Remove the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to remove.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option_at_index","title":"remove_option_at_index method","text":"
    def remove_option_at_index(self, index):\n

    Remove the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to remove.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt","title":"replace_option_prompt method","text":"
    def replace_option_prompt(self, option_id, prompt):\n

    Replace the prompt of the option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the option to replace the prompt of.

    prompt RenderableType required

    The new prompt for the option.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index method","text":"
    def replace_option_prompt_at_index(self, index, prompt):\n

    Replace the prompt of the option at the given index.

    Parameters Parameter Default Description index int required

    The index of the option to replace the prompt of.

    prompt RenderableType required

    The new prompt for the option.

    Returns Type Description Self

    The OptionList instance.

    Raises Type Description OptionDoesNotExist

    If there is no option with the given index.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.scroll_to_highlight","title":"scroll_to_highlight method","text":"
    def scroll_to_highlight(self, top=False):\n

    Ensure that the highlighted option is in view.

    Parameters Parameter Default Description top bool False

    Scroll highlight to top of the list.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.validate_highlighted","title":"validate_highlighted method","text":"
    def validate_highlighted(self, highlighted):\n

    Validate the highlighted property value on access.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_highlighted","title":"watch_highlighted method","text":"
    def watch_highlighted(self, highlighted):\n

    React to the highlighted option having changed.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_show_vertical_scrollbar","title":"watch_show_vertical_scrollbar method","text":"
    def watch_show_vertical_scrollbar(self):\n

    Handle the vertical scrollbar visibility status changing.

    show_vertical_scrollbar is watched because it has an impact on the available width in which to render the renderables that make up the options in the list. If a vertical scrollbar appears or disappears we need to recalculate all the lines that make up the list.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID class","text":"

    Bases: Exception

    Raised if a duplicate ID is used when adding options to an option list.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option","title":"Option class","text":"
    def __init__(self, prompt, id=None, disabled=False):\n

    Class that holds the details of an individual option.

    Parameters Parameter Default Description prompt RenderableType required

    The prompt for the option.

    id str | None None

    The optional ID for the option.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.id","title":"id property","text":"
    id: str | None\n

    The optional ID for the option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.prompt","title":"prompt property","text":"
    prompt: RenderableType\n

    The prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets._option_list.Option.set_prompt","title":"set_prompt method","text":"
    def set_prompt(self, prompt):\n

    Set the prompt for the option.

    Parameters Parameter Default Description prompt RenderableType required

    The new prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

    Bases: Exception

    Raised when a request has been made for an option that doesn't exist.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Separator","title":"Separator class","text":"

    Class used to add a separator to an OptionList.

    "},{"location":"widgets/placeholder/","title":"Placeholder","text":"

    Added in version 0.6.0

    A widget that is meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.

    The placeholder widget has variants that display different bits of useful information. Clicking a placeholder will cycle through its variants.

    • Focusable
    • Container
    "},{"location":"widgets/placeholder/#example","title":"Example","text":"

    The example below shows each placeholder variant.

    Outputplaceholder.pyplaceholder.tcss

    PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\n\n\nclass PlaceholderApp(App):\n    CSS_PATH = \"placeholder.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield VerticalScroll(\n            Container(\n                Placeholder(\"This is a custom label for p1.\", id=\"p1\"),\n                Placeholder(\"Placeholder p2 here!\", id=\"p2\"),\n                Placeholder(id=\"p3\"),\n                Placeholder(id=\"p4\"),\n                Placeholder(id=\"p5\"),\n                Placeholder(),\n                Horizontal(\n                    Placeholder(variant=\"size\", id=\"col1\"),\n                    Placeholder(variant=\"text\", id=\"col2\"),\n                    Placeholder(variant=\"size\", id=\"col3\"),\n                    id=\"c1\",\n                ),\n                id=\"bot\",\n            ),\n            Container(\n                Placeholder(variant=\"text\", id=\"left\"),\n                Placeholder(variant=\"size\", id=\"topright\"),\n                Placeholder(variant=\"text\", id=\"botright\"),\n                id=\"top\",\n            ),\n            id=\"content\",\n        )\n\n\nif __name__ == \"__main__\":\n    app = PlaceholderApp()\n    app.run()\n
    Placeholder {\n    height: 100%;\n}\n\n#top {\n    height: 50%;\n    width: 100%;\n    layout: grid;\n    grid-size: 2 2;\n}\n\n#left {\n    row-span: 2;\n}\n\n#bot {\n    height: 50%;\n    width: 100%;\n    layout: grid;\n    grid-size: 8 8;\n}\n\n#c1 {\n    row-span: 4;\n    column-span: 8;\n    height: 100%;\n}\n\n#col1, #col2, #col3 {\n    width: 1fr;\n}\n\n#p1 {\n    row-span: 4;\n    column-span: 4;\n}\n\n#p2 {\n    row-span: 2;\n    column-span: 4;\n}\n\n#p3 {\n    row-span: 2;\n    column-span: 2;\n}\n\n#p4 {\n    row-span: 1;\n    column-span: 2;\n}\n
    "},{"location":"widgets/placeholder/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description variant str \"default\" Styling variant. One of default, size, text."},{"location":"widgets/placeholder/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/placeholder/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/placeholder/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder","title":"textual.widgets.Placeholder class","text":"
    def __init__(\n    self,\n    label=None,\n    variant=\"default\",\n    *,\n    name=None,\n    id=None,\n    classes=None\n):\n

    Bases: Widget

    A simple placeholder widget to use before you build your custom widgets.

    This placeholder has a couple of variants that show different data. Clicking the placeholder cycles through the available variants, but a placeholder can also be initialised in a specific variant.

    The variants available are:

    Variant Placeholder shows default Identifier label or the ID of the placeholder. size Size of the placeholder. text Lorem Ipsum text. Parameters Parameter Default Description label str | None None

    The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

    variant PlaceholderVariant 'default'

    The variant of the placeholder.

    name str | None None

    The name of the placeholder.

    id str | None None

    The ID of the placeholder in the DOM.

    classes str | None None

    A space separated string with the CSS classes of the placeholder, if any.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.variant","title":"variant class-attribute instance-attribute","text":"
    variant: Reactive[PlaceholderVariant] = (\n    self.validate_variant(variant)\n)\n

    The current variant of the placeholder.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.cycle_variant","title":"cycle_variant method","text":"
    def cycle_variant(self):\n

    Get the next variant in the cycle.

    Returns Type Description Self

    The Placeholder instance.

    "},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.validate_variant","title":"validate_variant method","text":"
    def validate_variant(self, variant):\n

    Validate the variant to which the placeholder was set.

    "},{"location":"widgets/pretty/","title":"Pretty","text":"

    Display a pretty-formatted object.

    • Focusable
    • Container
    "},{"location":"widgets/pretty/#example","title":"Example","text":"

    The example below shows a pretty-formatted dict, but Pretty can display any Python object.

    Outputpretty.py

    PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

    from textual.app import App, ComposeResult\nfrom textual.widgets import Pretty\n\nDATA = {\n    \"title\": \"Back to the Future\",\n    \"releaseYear\": 1985,\n    \"director\": \"Robert Zemeckis\",\n    \"genre\": \"Adventure, Comedy, Sci-Fi\",\n    \"cast\": [\n        {\"actor\": \"Michael J. Fox\", \"character\": \"Marty McFly\"},\n        {\"actor\": \"Christopher Lloyd\", \"character\": \"Dr. Emmett Brown\"},\n    ],\n}\n\n\nclass PrettyExample(App):\n    def compose(self) -> ComposeResult:\n        yield Pretty(DATA)\n\n\napp = PrettyExample()\n\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/pretty/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/pretty/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/pretty/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/pretty/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/pretty/#textual.widgets.Pretty","title":"textual.widgets.Pretty class","text":"
    def __init__(self, object, *, name=None, id=None, classes=None):\n

    Bases: Widget

    A pretty-printing widget.

    Used to pretty-print any object.

    Parameters Parameter Default Description object Any required

    The object to pretty-print.

    name str | None None

    The name of the pretty widget.

    id str | None None

    The ID of the pretty in the DOM.

    classes str | None None

    The CSS classes of the pretty.

    "},{"location":"widgets/pretty/#textual.widgets._pretty.Pretty.update","title":"update method","text":"
    def update(self, object):\n

    Update the content of the pretty widget.

    Parameters Parameter Default Description object Any required

    The object to pretty-print.

    "},{"location":"widgets/progress_bar/","title":"ProgressBar","text":"

    A widget that displays progress on a time-consuming task.

    • Focusable
    • Container
    "},{"location":"widgets/progress_bar/#examples","title":"Examples","text":""},{"location":"widgets/progress_bar/#progress-bar-in-isolation","title":"Progress Bar in Isolation","text":"

    The example below shows a progress bar in isolation. It shows the progress bar in:

    • its indeterminate state, when the total progress hasn't been set yet;
    • the middle of the progress; and
    • the completed state.
    Indeterminate state39% doneCompletedprogress_bar_isolated.py

    IndeterminateProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

    IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\n\n\nclass IndeterminateProgressBar(App[None]):\n    BINDINGS = [(\"s\", \"start\", \"Start\")]\n\n    progress_timer: Timer\n    \"\"\"Timer to simulate progress happening.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            with Middle():\n                yield ProgressBar()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Set up a timer to simulate progess happening.\"\"\"\n        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\n\n    def make_progress(self) -> None:\n        \"\"\"Called automatically to advance the progress bar.\"\"\"\n        self.query_one(ProgressBar).advance(1)\n\n    def action_start(self) -> None:\n        \"\"\"Start the progress tracking.\"\"\"\n        self.query_one(ProgressBar).update(total=100)\n        self.progress_timer.resume()\n\n\nif __name__ == \"__main__\":\n    IndeterminateProgressBar().run()\n
    "},{"location":"widgets/progress_bar/#complete-app-example","title":"Complete App Example","text":"

    The example below shows a simple app with a progress bar that is keeping track of a fictitious funding level for an organisation.

    OutputOutput (partial funding)Output (full funding)progress_bar.pyprogress_bar.tcss

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u25010% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250135% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received!

    Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258eDonate \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received! Donation\u00a0for\u00a0$65\u00a0received!

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, VerticalScroll\nfrom textual.widgets import Button, Header, Input, Label, ProgressBar\n\n\nclass FundingProgressApp(App[None]):\n    CSS_PATH = \"progress_bar.tcss\"\n\n    TITLE = \"Funding tracking\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Center():\n            yield Label(\"Funding: \")\n            yield ProgressBar(total=100, show_eta=False)  # (1)!\n        with Center():\n            yield Input(placeholder=\"$$$\")\n            yield Button(\"Donate\")\n\n        yield VerticalScroll(id=\"history\")\n\n    def on_button_pressed(self) -> None:\n        self.add_donation()\n\n    def on_input_submitted(self) -> None:\n        self.add_donation()\n\n    def add_donation(self) -> None:\n        text_value = self.query_one(Input).value\n        try:\n            value = int(text_value)\n        except ValueError:\n            return\n        self.query_one(ProgressBar).advance(value)\n        self.query_one(VerticalScroll).mount(Label(f\"Donation for ${value} received!\"))\n        self.query_one(Input).value = \"\"\n\n\nif __name__ == \"__main__\":\n    FundingProgressApp().run()\n
    1. We create a progress bar with a total of 100 steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task.
    Container {\n    overflow: hidden hidden;\n    height: auto;\n}\n\nCenter {\n    margin-top: 1;\n    margin-bottom: 1;\n    layout: horizontal;\n}\n\nProgressBar {\n    padding-left: 3;\n}\n\nInput {\n    width: 16;\n}\n\nVerticalScroll {\n    height: auto;\n}\n
    "},{"location":"widgets/progress_bar/#custom-styling","title":"Custom Styling","text":"

    This shows a progress bar with custom styling. Refer to the section below for more information.

    Indeterminate state39% doneCompletedprogress_bar_styled.pyprogress_bar_styled.tcss

    StyledProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

    StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

    from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\n\n\nclass StyledProgressBar(App[None]):\n    BINDINGS = [(\"s\", \"start\", \"Start\")]\n    CSS_PATH = \"progress_bar_styled.tcss\"\n\n    progress_timer: Timer\n    \"\"\"Timer to simulate progress happening.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        with Center():\n            with Middle():\n                yield ProgressBar()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Set up a timer to simulate progess happening.\"\"\"\n        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\n\n    def make_progress(self) -> None:\n        \"\"\"Called automatically to advance the progress bar.\"\"\"\n        self.query_one(ProgressBar).advance(1)\n\n    def action_start(self) -> None:\n        \"\"\"Start the progress tracking.\"\"\"\n        self.query_one(ProgressBar).update(total=100)\n        self.progress_timer.resume()\n\n\nif __name__ == \"__main__\":\n    StyledProgressBar().run()\n
    Bar > .bar--indeterminate {\n    color: $primary;\n    background: $secondary;\n}\n\nBar > .bar--bar {\n    color: $primary;\n    background: $primary 30%;\n}\n\nBar > .bar--complete {\n    color: $error;\n}\n\nPercentageStatus {\n    text-style: reverse;\n    color: $secondary;\n}\n\nETAStatus {\n    text-style: underline;\n}\n
    "},{"location":"widgets/progress_bar/#styling-the-progress-bar","title":"Styling the Progress Bar","text":"

    The progress bar is composed of three sub-widgets that can be styled independently:

    Widget name ID Description Bar #bar The bar that visually represents the progress made. PercentageStatus #percentage Label that shows the percentage of completion. ETAStatus #eta Label that shows the estimated time to completion."},{"location":"widgets/progress_bar/#bar-component-classes","title":"Bar Component Classes","text":"

    The bar sub-widget provides the component classes that follow.

    These component classes let you modify the foreground and background color of the bar in its different states.

    Class Description bar--bar Style of the bar (may be used to change the color). bar--complete Style of the bar when it's complete. bar--indeterminate Style of the bar when it's in an indeterminate state."},{"location":"widgets/progress_bar/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description percentage float | None The read-only percentage of progress that has been made. This is None if the total hasn't been set. progress float 0 The number of steps of progress already made. total float | None The total number of steps that we are keeping track of."},{"location":"widgets/progress_bar/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/progress_bar/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/progress_bar/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar","title":"textual.widgets.ProgressBar class","text":"
    def __init__(\n    self,\n    total=None,\n    *,\n    show_bar=True,\n    show_percentage=True,\n    show_eta=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A progress bar widget.

    The progress bar uses \"steps\" as the measurement unit.

    Example
    class MyApp(App):\n    def compose(self):\n        yield ProgressBar(total=100)\n\n    def key_space(self):\n        self.query_one(ProgressBar).advance(5)\n
    Parameters Parameter Default Description total float | None None

    The total number of steps in the progress if known.

    show_bar bool True

    Whether to show the bar portion of the progress bar.

    show_percentage bool True

    Whether to show the percentage status of the bar.

    show_eta bool True

    Whether to show the ETA countdown of the progress bar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.percentage","title":"percentage class-attribute instance-attribute","text":"
    percentage: reactive[float | None] = reactive[\n    Optional[float]\n](None)\n

    The percentage of progress that has been completed.

    The percentage is a value between 0 and 1 and the returned value is only None if the total progress of the bar hasn't been set yet.

    Example
    progress_bar = ProgressBar()\nprint(progress_bar.percentage)  # None\nprogress_bar.update(total=100)\nprogress_bar.advance(50)\nprint(progress_bar.percentage)  # 0.5\n
    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.progress","title":"progress class-attribute instance-attribute","text":"
    progress: reactive[float] = reactive(0.0)\n

    The progress so far, in number of steps.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.total","title":"total class-attribute instance-attribute","text":"
    total: reactive[float | None] = total\n

    The total number of steps associated with this progress bar, when known.

    The value None will render an indeterminate progress bar.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.advance","title":"advance method","text":"
    def advance(self, advance=1):\n

    Advance the progress of the progress bar by the given amount.

    Example
    progress_bar.advance(10)  # Advance 10 steps.\n
    Parameters Parameter Default Description advance float 1

    Number of steps to advance progress by.

    "},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.update","title":"update method","text":"
    def update(\n    self, *, total=UNUSED, progress=UNUSED, advance=UNUSED\n):\n

    Update the progress bar with the given options.

    Example
    progress_bar.update(\n    total=200,  # Set new total to 200 steps.\n    progress=50,  # Set the progress to 50 (out of 200).\n)\n
    Parameters Parameter Default Description total None | float | UnusedParameter UNUSED

    New total number of steps.

    progress float | UnusedParameter UNUSED

    Set the progress to the given number of steps.

    advance float | UnusedParameter UNUSED

    Advance the progress by this number of steps.

    "},{"location":"widgets/radiobutton/","title":"RadioButton","text":"

    Added in version 0.13.0

    A simple radio button which stores a boolean value.

    • Focusable
    • Container

    A radio button is best used with others inside a RadioSet.

    "},{"location":"widgets/radiobutton/#example","title":"Example","text":"

    The example below shows radio buttons, used within a RadioSet.

    Outputradio_button.pyradio_button.tcss

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import RadioButton, RadioSet\n\n\nclass RadioChoicesApp(App[None]):\n    CSS_PATH = \"radio_button.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with RadioSet():\n            yield RadioButton(\"Battlestar Galactica\")\n            yield RadioButton(\"Dune 1984\")\n            yield RadioButton(\"Dune 2021\", id=\"focus_me\")\n            yield RadioButton(\"Serenity\", value=True)\n            yield RadioButton(\"Star Trek: The Motion Picture\")\n            yield RadioButton(\"Star Wars: A New Hope\")\n            yield RadioButton(\"The Last Starfighter\")\n            yield RadioButton(\n                \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n            )\n            yield RadioButton(\"Wing Commander\")\n\n    def on_mount(self) -> None:\n        self.query_one(RadioSet).focus()\n\n\nif __name__ == \"__main__\":\n    RadioChoicesApp().run()\n
    Screen {\n    align: center middle;\n}\n\nRadioSet {\n    width: 50%;\n}\n
    "},{"location":"widgets/radiobutton/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the radio button."},{"location":"widgets/radiobutton/#messages","title":"Messages","text":"
    • RadioButton.Changed
    "},{"location":"widgets/radiobutton/#bindings","title":"Bindings","text":"

    The radio button widget defines the following bindings:

    Key(s) Description enter, space Toggle the value."},{"location":"widgets/radiobutton/#component-classes","title":"Component Classes","text":"

    The checkbox widget inherits the following component classes:

    Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/radiobutton/#see-also","title":"See Also","text":"
    • RadioSet
    "},{"location":"widgets/radiobutton/#textual.widgets.RadioButton","title":"textual.widgets.RadioButton class","text":"

    Bases: ToggleButton

    A radio button widget that represents a boolean value.

    Note

    A RadioButton is best used within a RadioSet.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.BUTTON_INNER","title":"BUTTON_INNER class-attribute instance-attribute","text":"
    BUTTON_INNER = '\u25cf'\n

    The character used for the inside of the button.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed","title":"Changed class","text":"

    Bases: ToggleButton.Changed

    Posted when the value of the radio button changes.

    This message can be handled using an on_radio_button_changed method.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.control","title":"control property","text":"
    control: RadioButton\n

    Alias for Changed.radio_button.

    "},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.radio_button","title":"radio_button property","text":"
    radio_button: RadioButton\n

    The radio button that was changed.

    "},{"location":"widgets/radioset/","title":"RadioSet","text":"

    Added in version 0.13.0

    A container widget that groups RadioButtons together.

    • Focusable
    • Container
    "},{"location":"widgets/radioset/#example","title":"Example","text":""},{"location":"widgets/radioset/#simple-example","title":"Simple example","text":"

    The example below shows two radio sets, one built using a collection of radio buttons, the other a collection of simple strings.

    Outputradio_set.pyradio_set.tcss

    RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Pictur\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import RadioButton, RadioSet\n\n\nclass RadioChoicesApp(App[None]):\n    CSS_PATH = \"radio_set.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            # A RadioSet built up from RadioButtons.\n            with RadioSet(id=\"focus_me\"):\n                yield RadioButton(\"Battlestar Galactica\")\n                yield RadioButton(\"Dune 1984\")\n                yield RadioButton(\"Dune 2021\")\n                yield RadioButton(\"Serenity\", value=True)\n                yield RadioButton(\"Star Trek: The Motion Picture\")\n                yield RadioButton(\"Star Wars: A New Hope\")\n                yield RadioButton(\"The Last Starfighter\")\n                yield RadioButton(\n                    \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n                )\n                yield RadioButton(\"Wing Commander\")\n            # A RadioSet built up from a collection of strings.\n            yield RadioSet(\n                \"Amanda\",\n                \"Connor MacLeod\",\n                \"Duncan MacLeod\",\n                \"Heather MacLeod\",\n                \"Joe Dawson\",\n                \"Kurgan, [bold italic red]The[/]\",\n                \"Methos\",\n                \"Rachel Ellenstein\",\n                \"Ram\u00edrez\",\n            )\n\n    def on_mount(self) -> None:\n        self.query_one(\"#focus_me\").focus()\n\n\nif __name__ == \"__main__\":\n    RadioChoicesApp().run()\n
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    align: center middle;\n    height: auto;\n}\n\nRadioSet {\n    width: 45%;\n}\n
    "},{"location":"widgets/radioset/#reacting-to-changes-in-a-radio-set","title":"Reacting to Changes in a Radio Set","text":"

    Here is an example of using the message to react to changes in a RadioSet:

    Outputradio_set_changed.pyradio_set_changed.tcss

    RadioSetChangedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Pictu\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2587\u2587 Pressed\u00a0button\u00a0label:\u00a0Battlestar\u00a0Galactica

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Label, RadioButton, RadioSet\n\n\nclass RadioSetChangedApp(App[None]):\n    CSS_PATH = \"radio_set_changed.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with VerticalScroll():\n            with Horizontal():\n                with RadioSet(id=\"focus_me\"):\n                    yield RadioButton(\"Battlestar Galactica\")\n                    yield RadioButton(\"Dune 1984\")\n                    yield RadioButton(\"Dune 2021\")\n                    yield RadioButton(\"Serenity\", value=True)\n                    yield RadioButton(\"Star Trek: The Motion Picture\")\n                    yield RadioButton(\"Star Wars: A New Hope\")\n                    yield RadioButton(\"The Last Starfighter\")\n                    yield RadioButton(\n                        \"Total Recall :backhand_index_pointing_right: :red_circle:\"\n                    )\n                    yield RadioButton(\"Wing Commander\")\n            with Horizontal():\n                yield Label(id=\"pressed\")\n            with Horizontal():\n                yield Label(id=\"index\")\n\n    def on_mount(self) -> None:\n        self.query_one(RadioSet).focus()\n\n    def on_radio_set_changed(self, event: RadioSet.Changed) -> None:\n        self.query_one(\"#pressed\", Label).update(\n            f\"Pressed button label: {event.pressed.label}\"\n        )\n        self.query_one(\"#index\", Label).update(\n            f\"Pressed button index: {event.radio_set.pressed_index}\"\n        )\n\n\nif __name__ == \"__main__\":\n    RadioSetChangedApp().run()\n
    VerticalScroll {\n    align: center middle;\n}\n\nHorizontal {\n    align: center middle;\n    height: auto;\n}\n\nRadioSet {\n    width: 45%;\n}\n
    "},{"location":"widgets/radioset/#messages","title":"Messages","text":"
    • RadioSet.Changed
    "},{"location":"widgets/radioset/#bindings","title":"Bindings","text":"

    The RadioSet widget defines the following bindings:

    Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/radioset/#see-also","title":"See Also","text":"
    • RadioButton
    "},{"location":"widgets/radioset/#textual.widgets.RadioSet","title":"textual.widgets.RadioSet class","text":"
    def __init__(\n    self,\n    *buttons,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Container

    Widget for grouping a collection of radio buttons into a set.

    When a collection of RadioButtons are grouped with this widget, they will be treated as a mutually-exclusive grouping. If one button is turned on, the previously-on button will be turned off.

    Parameters Parameter Default Description buttons str | RadioButton ()

    The labels or RadioButtons to group together.

    name str | None None

    The name of the radio set.

    id str | None None

    The ID of the radio set in the DOM.

    classes str | None None

    The CSS classes of the radio set.

    disabled bool False

    Whether the radio set is disabled or not.

    Note

    When a str label is provided, a RadioButton will be created from it.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"down,right\", \"next_button\", \"\", show=False),\n    Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\n    Binding(\"up,left\", \"previous_button\", \"\", show=False),\n]\n
    Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_button","title":"pressed_button property","text":"
    pressed_button: RadioButton | None\n

    The currently-pressed RadioButton, or None if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_index","title":"pressed_index property","text":"
    pressed_index: int\n

    The index of the currently-pressed RadioButton, or -1 if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed","title":"Changed class","text":"
    def __init__(self, radio_set, pressed):\n

    Bases: Message

    Posted when the pressed button in the set changes.

    This message can be handled using an on_radio_set_changed method.

    Parameters Parameter Default Description pressed RadioButton required

    The radio button that was pressed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'pressed'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.control","title":"control property","text":"
    control: RadioSet\n

    A reference to the RadioSet that was changed.

    This is an alias for Changed.radio_set and is used by the on decorator.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.index","title":"index instance-attribute","text":"
    index = radio_set.pressed_index\n

    The index of the RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.pressed","title":"pressed instance-attribute","text":"
    pressed = pressed\n

    The RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.radio_set","title":"radio_set instance-attribute","text":"
    radio_set = radio_set\n

    A reference to the RadioSet that was changed.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_next_button","title":"action_next_button method","text":"
    def action_next_button(self):\n

    Navigate to the next button in the set.

    Note that this will wrap around to the start if at the end.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_previous_button","title":"action_previous_button method","text":"
    def action_previous_button(self):\n

    Navigate to the previous button in the set.

    Note that this will wrap around to the end if at the start.

    "},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_toggle","title":"action_toggle method","text":"
    def action_toggle(self):\n

    Toggle the state of the currently-selected button.

    "},{"location":"widgets/rich_log/","title":"RichLog","text":"

    A RichLog is a widget which displays scrollable content that may be appended to in realtime.

    Call RichLog.write with a string or Rich Renderable to write content to the end of the RichLog. Call RichLog.clear to clear the content.

    Tip

    See also Log which is an alternative to RichLog but specialized for simple text.

    • Focusable
    • Container
    "},{"location":"widgets/rich_log/#example","title":"Example","text":"

    The example below shows an application showing a RichLog with different kinds of data logged.

    Outputrich_log.py

    RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

    import csv\nimport io\n\nfrom rich.syntax import Syntax\nfrom rich.table import Table\n\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\n\nCSV = \"\"\"lane,swimmer,country,time\n4,Joseph Schooling,Singapore,50.39\n2,Michael Phelps,United States,51.14\n5,Chad le Clos,South Africa,51.14\n6,L\u00e1szl\u00f3 Cseh,Hungary,51.14\n3,Li Zhuhao,China,51.26\n8,Mehdy Metella,France,51.58\n7,Tom Shields,United States,51.73\n1,Aleksandr Sadovnikov,Russia,51.84\"\"\"\n\n\nCODE = '''\\\ndef loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:\n    \"\"\"Iterate and generate a tuple with a flag for first and last value.\"\"\"\n    iter_values = iter(values)\n    try:\n        previous_value = next(iter_values)\n    except StopIteration:\n        return\n    first = True\n    for value in iter_values:\n        yield first, False, previous_value\n        first = False\n        previous_value = value\n    yield first, True, previous_value\\\n'''\n\n\nclass RichLogApp(App):\n    def compose(self) -> ComposeResult:\n        yield RichLog(highlight=True, markup=True)\n\n    def on_ready(self) -> None:\n        \"\"\"Called  when the DOM is ready.\"\"\"\n        text_log = self.query_one(RichLog)\n\n        text_log.write(Syntax(CODE, \"python\", indent_guides=True))\n\n        rows = iter(csv.reader(io.StringIO(CSV)))\n        table = Table(*next(rows))\n        for row in rows:\n            table.add_row(*row)\n\n        text_log.write(table)\n        text_log.write(\"[bold magenta]Write text or any Rich renderable!\")\n\n    def on_key(self, event: events.Key) -> None:\n        \"\"\"Write Key events to log.\"\"\"\n        text_log = self.query_one(RichLog)\n        text_log.write(event)\n\n\nif __name__ == \"__main__\":\n    app = RichLogApp()\n    app.run()\n
    "},{"location":"widgets/rich_log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight bool False Automatically highlight content. markup bool False Apply Rich console markup. max_lines int None Maximum number of lines in the log or None for no maximum. min_width int 78 Minimum width of renderables. wrap bool False Enable word wrapping."},{"location":"widgets/rich_log/#messages","title":"Messages","text":"

    This widget sends no messages.

    "},{"location":"widgets/rich_log/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/rich_log/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog","title":"textual.widgets.RichLog class","text":"
    def __init__(\n    self,\n    *,\n    max_lines=None,\n    min_width=78,\n    wrap=False,\n    highlight=False,\n    markup=False,\n    auto_scroll=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView

    A widget for logging text.

    Parameters Parameter Default Description max_lines int | None None

    Maximum number of lines in the log or None for no maximum.

    min_width int 78

    Minimum width of renderables.

    wrap bool False

    Enable word wrapping (default is off).

    highlight bool False

    Automatically highlight content.

    markup bool False

    Apply Rich console markup.

    auto_scroll bool True

    Enable automatic scrolling to end.

    name str | None None

    The name of the text log.

    id str | None None

    The ID of the text log in the DOM.

    classes str | None None

    The CSS classes of the text log.

    disabled bool False

    Whether the text log is disabled or not.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll: var[bool] = auto_scroll\n

    Automatically scroll to the end on write.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.highlight","title":"highlight class-attribute instance-attribute","text":"
    highlight: var[bool] = highlight\n

    Automatically highlight content.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.markup","title":"markup class-attribute instance-attribute","text":"
    markup: var[bool] = markup\n

    Apply Rich console markup.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines: var[int | None] = max_lines\n

    Maximum number of lines in the log or None for no maximum.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.min_width","title":"min_width class-attribute instance-attribute","text":"
    min_width: var[int] = min_width\n

    Minimum width of renderables.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.wrap","title":"wrap class-attribute instance-attribute","text":"
    wrap: var[bool] = wrap\n

    Enable word wrapping.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the text log.

    Returns Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.write","title":"write method","text":"
    def write(\n    self,\n    content,\n    width=None,\n    expand=False,\n    shrink=True,\n    scroll_end=None,\n):\n

    Write text or a rich renderable.

    Parameters Parameter Default Description content RenderableType | object required

    Rich renderable (or text).

    width int | None None

    Width to render or None to use optimal width.

    expand bool False

    Enable expand to widget width, or False to use width.

    shrink bool True

    Enable shrinking of content to fit width.

    scroll_end bool | None None

    Enable automatic scroll to end, or None to use self.auto_scroll.

    Returns Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rule/","title":"Rule","text":"

    A rule widget to separate content, similar to a <hr> HTML tag.

    • Focusable
    • Container
    "},{"location":"widgets/rule/#examples","title":"Examples","text":""},{"location":"widgets/rule/#horizontal-rule","title":"Horizontal Rule","text":"

    The default orientation of a rule is horizontal.

    The example below shows horizontal rules with all the available line styles.

    Outputhorizontal_rules.pyhorizontal_rules.tcss

    HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

    from textual.app import App, ComposeResult\nfrom textual.containers import Vertical\nfrom textual.widgets import Label, Rule\n\n\nclass HorizontalRulesApp(App):\n    CSS_PATH = \"horizontal_rules.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Vertical():\n            yield Label(\"solid (default)\")\n            yield Rule()\n            yield Label(\"heavy\")\n            yield Rule(line_style=\"heavy\")\n            yield Label(\"thick\")\n            yield Rule(line_style=\"thick\")\n            yield Label(\"dashed\")\n            yield Rule(line_style=\"dashed\")\n            yield Label(\"double\")\n            yield Rule(line_style=\"double\")\n            yield Label(\"ascii\")\n            yield Rule(line_style=\"ascii\")\n\n\nif __name__ == \"__main__\":\n    app = HorizontalRulesApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nVertical {\n    height: auto;\n    width: 80%;\n}\n\nLabel {\n    width: 100%;\n    text-align: center;\n}\n
    "},{"location":"widgets/rule/#vertical-rule","title":"Vertical Rule","text":"

    The example below shows vertical rules with all the available line styles.

    Outputvertical_rules.pyvertical_rules.tcss

    VerticalRulesApp solid\u00a0\u2502heavy\u00a0\u2503thick\u00a0\u2588dashed\u254fdouble\u2551ascii\u00a0| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551|

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Rule\n\n\nclass VerticalRulesApp(App):\n    CSS_PATH = \"vertical_rules.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            yield Label(\"solid\")\n            yield Rule(orientation=\"vertical\")\n            yield Label(\"heavy\")\n            yield Rule(orientation=\"vertical\", line_style=\"heavy\")\n            yield Label(\"thick\")\n            yield Rule(orientation=\"vertical\", line_style=\"thick\")\n            yield Label(\"dashed\")\n            yield Rule(orientation=\"vertical\", line_style=\"dashed\")\n            yield Label(\"double\")\n            yield Rule(orientation=\"vertical\", line_style=\"double\")\n            yield Label(\"ascii\")\n            yield Rule(orientation=\"vertical\", line_style=\"ascii\")\n\n\nif __name__ == \"__main__\":\n    app = VerticalRulesApp()\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    width: auto;\n    height: 80%;\n}\n\nLabel {\n    width: 6;\n    height: 100%;\n    text-align: center;\n}\n
    "},{"location":"widgets/rule/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description orientation RuleOrientation \"horizontal\" The orientation of the rule. line_style LineStyle \"solid\" The line style of the rule."},{"location":"widgets/rule/#messages","title":"Messages","text":"

    This widget sends no messages.

    "},{"location":"widgets/rule/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/rule/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/rule/#textual.widgets.Rule","title":"textual.widgets.Rule class","text":"
    def __init__(\n    self,\n    orientation=\"horizontal\",\n    line_style=\"solid\",\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A rule widget to separate content, similar to a <hr> HTML tag.

    Parameters Parameter Default Description orientation RuleOrientation 'horizontal'

    The orientation of the rule.

    line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.line_style","title":"line_style class-attribute instance-attribute","text":"
    line_style: Reactive[LineStyle] = line_style\n

    The line style of the rule.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.orientation","title":"orientation class-attribute instance-attribute","text":"
    orientation: Reactive[RuleOrientation] = orientation\n

    The orientation of the rule.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.horizontal","title":"horizontal classmethod","text":"
    def horizontal(\n    cls,\n    line_style=\"solid\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Utility constructor for creating a horizontal rule.

    Parameters Parameter Default Description line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    Returns Type Description Rule

    A rule widget with horizontal orientation.

    "},{"location":"widgets/rule/#textual.widgets._rule.Rule.vertical","title":"vertical classmethod","text":"
    def vertical(\n    cls,\n    line_style=\"solid\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n):\n

    Utility constructor for creating a vertical rule.

    Parameters Parameter Default Description line_style LineStyle 'solid'

    The line style of the rule.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes of the widget.

    disabled bool False

    Whether the widget is disabled or not.

    Returns Type Description Rule

    A rule widget with vertical orientation.

    "},{"location":"widgets/rule/#textual.widgets.rule","title":"textual.widgets.rule","text":""},{"location":"widgets/rule/#textual.widgets.rule.LineStyle","title":"LineStyle module-attribute","text":"
    LineStyle = Literal[\n    \"ascii\",\n    \"blank\",\n    \"dashed\",\n    \"double\",\n    \"heavy\",\n    \"hidden\",\n    \"none\",\n    \"solid\",\n    \"thick\",\n]\n

    The valid line styles of the rule widget.

    "},{"location":"widgets/rule/#textual.widgets.rule.RuleOrientation","title":"RuleOrientation module-attribute","text":"
    RuleOrientation = Literal['horizontal', 'vertical']\n

    The valid orientations of the rule widget.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidLineStyle","title":"InvalidLineStyle class","text":"

    Bases: Exception

    Exception raised for an invalid rule line style.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation class","text":"

    Bases: Exception

    Exception raised for an invalid rule orientation.

    "},{"location":"widgets/select/","title":"Select","text":"

    Added in version 0.24.0

    A Select widget is a compact control to allow the user to select between a number of possible options.

    • Focusable
    • Container

    The options in a select control may be passed in to the constructor or set later with set_options. Options should be given as a sequence of tuples consisting of two values: the first is the string (or Rich Renderable) to display in the control and list of options, the second is the value of option.

    The value of the currently selected option is stored in the value attribute of the widget, and the value attribute of the Changed message.

    "},{"location":"widgets/select/#typing","title":"Typing","text":"

    The Select control is a typing Generic which allows you to set the type of the option values. For instance, if the data type for your values is an integer, you would type the widget as follows:

    options = [(\"First\", 1), (\"Second\", 2)]\nmy_select: Select[int] =  Select(options)\n

    Note

    Typing is entirely optional.

    If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

    "},{"location":"widgets/select/#examples","title":"Examples","text":""},{"location":"widgets/select/#basic-example","title":"Basic Example","text":"

    The following example presents a Select with a number of options.

    OutputOutput (expanded)select_widget.pyselect.tcss

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select((line, line) for line in LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    Screen {\n    align: center top;\n}\n\nSelect {\n    width: 60;\n    margin: 2;\n}\n
    "},{"location":"widgets/select/#example-using-class-method","title":"Example using Class Method","text":"

    The following example presents a Select created using the from_values class method.

    OutputOutput (expanded)select_from_values_widget.pyselect.tcss

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\n\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\n\n\nclass SelectApp(App):\n    CSS_PATH = \"select.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield Select.from_values(LINES)\n\n    @on(Select.Changed)\n    def select_changed(self, event: Select.Changed) -> None:\n        self.title = str(event.value)\n\n\nif __name__ == \"__main__\":\n    app = SelectApp()\n    app.run()\n
    Screen {\n    align: center top;\n}\n\nSelect {\n    width: 60;\n    margin: 2;\n}\n
    "},{"location":"widgets/select/#blank-state","title":"Blank state","text":"

    The widget Select has an option allow_blank for its constructor. If set to True, the widget may be in a state where there is no selection, in which case its value will be the special constant Select.BLANK. The auxiliary methods Select.is_blank and Select.clear provide a convenient way to check if the widget is in this state and to set this state, respectively.

    "},{"location":"widgets/select/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description expanded bool False True to expand the options overlay. value SelectType | _NoSelection Select.BLANK Current value of the Select."},{"location":"widgets/select/#messages","title":"Messages","text":"
    • Select.Changed
    "},{"location":"widgets/select/#bindings","title":"Bindings","text":"

    The Select widget defines the following bindings:

    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/select/#textual.widgets.Select","title":"textual.widgets.Select class","text":"
    def __init__(\n    self,\n    options,\n    *,\n    prompt=\"Select\",\n    allow_blank=True,\n    value=BLANK,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[SelectType], Vertical

    Widget to select from a list of possible options.

    A Select displays the current selection. When activated with Enter the widget displays an overlay with a list of all possible options.

    Parameters Parameter Default Description options Iterable[tuple[RenderableType, SelectType]] required

    Options to select from. If no options are provided then allow_blank must be set to True.

    prompt str 'Select'

    Text to show in the control when no option is selected.

    allow_blank bool True

    Enables or disables the ability to have the widget in a state with no selection made, in which case its value is set to the constant Select.BLANK.

    value SelectType | NoSelection BLANK

    Initial value selected. Should be one of the values in options. If no initial value is set and allow_blank is False, the widget will auto-select the first available option.

    name str | None None

    The name of the select control.

    id str | None None

    The ID of the control in the DOM.

    classes str | None None

    The CSS classes of the control.

    disabled bool False

    Whether the control is disabled or not.

    Raises Type Description EmptySelectError

    If no options are provided and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [('enter,down,space,up', 'show_overlay')]\n
    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets._select.Select.BLANK","title":"BLANK class-attribute instance-attribute","text":"
    BLANK = BLANK\n

    Constant to flag that the widget has no selection.

    "},{"location":"widgets/select/#textual.widgets._select.Select.expanded","title":"expanded class-attribute instance-attribute","text":"
    expanded: var[bool] = var(False, init=False)\n

    True to show the overlay, otherwise False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.prompt","title":"prompt class-attribute instance-attribute","text":"
    prompt: var[str] = prompt\n

    The prompt to show when no value is selected.

    "},{"location":"widgets/select/#textual.widgets._select.Select.value","title":"value class-attribute instance-attribute","text":"
    value: var[SelectType | NoSelection] = var[\n    Union[SelectType, NoSelection]\n](BLANK)\n

    The value of the selection.

    If the widget has no selection, its value will be Select.BLANK. Setting this to an illegal value will raise a InvalidSelectValueError exception.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed","title":"Changed class","text":"
    def __init__(self, select, value):\n

    Bases: Message

    Posted when the select value was changed.

    This message can be handled using a on_select_changed method.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.control","title":"control property","text":"
    control: Select[SelectType]\n

    The Select that sent the message.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.select","title":"select instance-attribute","text":"
    select = select\n

    The select widget.

    "},{"location":"widgets/select/#textual.widgets._select.Select.Changed.value","title":"value instance-attribute","text":"
    value = value\n

    The value of the Select when it changed.

    "},{"location":"widgets/select/#textual.widgets._select.Select.action_show_overlay","title":"action_show_overlay method","text":"
    def action_show_overlay(self):\n

    Show the overlay.

    "},{"location":"widgets/select/#textual.widgets._select.Select.clear","title":"clear method","text":"
    def clear(self):\n

    Clear the selection if allow_blank is True.

    Raises Type Description InvalidSelectValueError

    If allow_blank is set to False.

    "},{"location":"widgets/select/#textual.widgets._select.Select.from_values","title":"from_values classmethod","text":"
    def from_values(\n    cls,\n    values,\n    *,\n    prompt=\"Select\",\n    allow_blank=True,\n    value=BLANK,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Initialize the Select control with values specified by an arbitrary iterable

    The options shown in the control are computed by calling the built-in str on each value.

    Parameters Parameter Default Description values Iterable[SelectType] required

    Values used to generate options to select from.

    prompt str 'Select'

    Text to show in the control when no option is selected.

    allow_blank bool True

    Enables or disables the ability to have the widget in a state with no selection made, in which case its value is set to the constant Select.BLANK.

    value SelectType | NoSelection BLANK

    Initial value selected. Should be one of the values in values. If no initial value is set and allow_blank is False, the widget will auto-select the first available value.

    name str | None None

    The name of the select control.

    id str | None None

    The ID of the control in the DOM.

    classes str | None None

    The CSS classes of the control.

    disabled bool False

    Whether the control is disabled or not.

    Returns Type Description Select[SelectType]

    A new Select widget with the provided values as options.

    "},{"location":"widgets/select/#textual.widgets._select.Select.is_blank","title":"is_blank method","text":"
    def is_blank(self):\n

    Indicates whether this Select is blank or not.

    Returns Type Description bool

    True if the selection is blank, False otherwise.

    "},{"location":"widgets/select/#textual.widgets._select.Select.set_options","title":"set_options method","text":"
    def set_options(self, options):\n

    Set the options for the Select.

    This will reset the selection. The selection will be empty, if allowed, otherwise the first valid option is picked.

    Parameters Parameter Default Description options Iterable[tuple[RenderableType, SelectType]] required

    An iterable of tuples containing the renderable to display for each option and the corresponding internal value.

    Raises Type Description EmptySelectError

    If the options iterable is empty and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.select.EmptySelectError","title":"EmptySelectError class","text":"

    Bases: Exception

    Raised when a Select has no options and allow_blank=False.

    "},{"location":"widgets/select/#textual.widgets.select.InvalidSelectValueError","title":"InvalidSelectValueError class","text":"

    Bases: Exception

    Raised when setting a Select to an unknown option.

    "},{"location":"widgets/selection_list/","title":"SelectionList","text":"

    Added in version 0.27.0

    A widget for showing a vertical list of selectable options.

    • Focusable
    • Container
    "},{"location":"widgets/selection_list/#typing","title":"Typing","text":"

    The SelectionList control is a Generic, which allows you to set the type of the selection values. For instance, if the data type for your values is an integer, you would type the widget as follows:

    selections = [(\"First\", 1), (\"Second\", 2)]\nmy_selection_list: SelectionList[int] =  SelectionList(*selections)\n

    Note

    Typing is entirely optional.

    If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

    "},{"location":"widgets/selection_list/#examples","title":"Examples","text":"

    A selection list is designed to be built up of single-line prompts (which can be Rich renderables) and an associated unique value.

    "},{"location":"widgets/selection_list/#selections-as-tuples","title":"Selections as tuples","text":"

    A selection list can be built with tuples, either of two or three values in length. Each tuple must contain a prompt and a value, and it can also optionally contain a flag for the initial selected state of the option.

    Outputselection_list_tuples.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield SelectionList[int](  # (1)!\n            (\"Falken's Maze\", 0, True),\n            (\"Black Jack\", 1),\n            (\"Gin Rummy\", 2),\n            (\"Hearts\", 3),\n            (\"Bridge\", 4),\n            (\"Checkers\", 5),\n            (\"Chess\", 6, True),\n            (\"Poker\", 7),\n            (\"Fighter Combat\", 8, True),\n        )\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as int, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 80%;\n    height: 80%;\n}\n
    "},{"location":"widgets/selection_list/#selections-as-selection-objects","title":"Selections as Selection objects","text":"

    Alternatively, selections can be passed in as Selections:

    Outputselection_list_selections.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nfrom textual.widgets.selection_list import Selection\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield SelectionList[int](  # (1)!\n            Selection(\"Falken's Maze\", 0, True),\n            Selection(\"Black Jack\", 1),\n            Selection(\"Gin Rummy\", 2),\n            Selection(\"Hearts\", 3),\n            Selection(\"Bridge\", 4),\n            Selection(\"Checkers\", 5),\n            Selection(\"Chess\", 6, True),\n            Selection(\"Poker\", 7),\n            Selection(\"Fighter Combat\", 8, True),\n        )\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as int, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 80%;\n    height: 80%;\n}\n
    "},{"location":"widgets/selection_list/#handling-changes-to-the-selections","title":"Handling changes to the selections","text":"

    Most of the time, when using the SelectionList, you will want to know when the collection of selected items has changed; this is ideally done using the SelectedChanged message. Here is an example of using that message to update a Pretty with the collection of selected values:

    Outputselection_list_selections.pyselection_list.tcss

    SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2510\u250c\u2500\u00a0Selected\u00a0games\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502[\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502\u2502'secret_back_door',\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502\u2502'a_nice_game_of_chess',\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502\u2502'fighter_combat'\u2502 \u2502\u2590X\u258cHearts\u2502\u2502]\u2502 \u2502\u2590X\u258cBridge\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

    from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.events import Mount\nfrom textual.widgets import Footer, Header, Pretty, SelectionList\nfrom textual.widgets.selection_list import Selection\n\n\nclass SelectionListApp(App[None]):\n    CSS_PATH = \"selection_list_selected.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Horizontal():\n            yield SelectionList[str](  # (1)!\n                Selection(\"Falken's Maze\", \"secret_back_door\", True),\n                Selection(\"Black Jack\", \"black_jack\"),\n                Selection(\"Gin Rummy\", \"gin_rummy\"),\n                Selection(\"Hearts\", \"hearts\"),\n                Selection(\"Bridge\", \"bridge\"),\n                Selection(\"Checkers\", \"checkers\"),\n                Selection(\"Chess\", \"a_nice_game_of_chess\", True),\n                Selection(\"Poker\", \"poker\"),\n                Selection(\"Fighter Combat\", \"fighter_combat\", True),\n            )\n            yield Pretty([])\n        yield Footer()\n\n    def on_mount(self) -> None:\n        self.query_one(SelectionList).border_title = \"Shall we play some games?\"\n        self.query_one(Pretty).border_title = \"Selected games\"\n\n    @on(Mount)\n    @on(SelectionList.SelectedChanged)\n    def update_selected_view(self) -> None:\n        self.query_one(Pretty).update(self.query_one(SelectionList).selected)\n\n\nif __name__ == \"__main__\":\n    SelectionListApp().run()\n
    1. Note that the SelectionList is typed as str, for the type of the values.
    Screen {\n    align: center middle;\n}\n\nHorizontal {\n    width: 80%;\n    height: 80%;\n}\n\nSelectionList {\n    padding: 1;\n    border: solid $accent;\n    width: 1fr;\n}\n\nPretty {\n    width: 1fr;\n    border: solid $accent;\n}\n
    "},{"location":"widgets/selection_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted selection. None means nothing is highlighted."},{"location":"widgets/selection_list/#messages","title":"Messages","text":"

    The following messages will be posted as the user interacts with the list:

    • SelectionList.SelectionHighlighted
    • SelectionList.SelectionToggled

    The following message will be posted if the content of selected changes, either by user interaction or by API calls:

    • SelectionList.SelectedChanged
    "},{"location":"widgets/selection_list/#bindings","title":"Bindings","text":"

    The selection list widget defines the following bindings:

    Key(s) Description space Toggle the state of the highlighted selection.

    It inherits from OptionList and so also inherits the following bindings:

    Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/selection_list/#component-classes","title":"Component Classes","text":"

    The selection list provides the following component classes:

    Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style.

    It inherits from OptionList and so also makes use of the following component classes:

    Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/selection_list/#textual.widgets.SelectionList","title":"textual.widgets.SelectionList class","text":"
    def __init__(\n    self,\n    *selections,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[SelectionType], OptionList

    A vertical selection list that allows making multiple selections.

    Parameters Parameter Default Description *selections Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool] ()

    The content for the selection list.

    name str | None None

    The name of the selection list.

    id str | None None

    The ID of the selection list in the DOM.

    classes str | None None

    The CSS classes of the selection list.

    disabled bool False

    Whether the selection list is disabled or not.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [Binding('space', 'select')]\n
    Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"selection-list--button\",\n    \"selection-list--button-selected\",\n    \"selection-list--button-highlighted\",\n    \"selection-list--button-selected-highlighted\",\n}\n
    Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.selected","title":"selected property","text":"
    selected: list[SelectionType]\n

    The selected values.

    This is a list of all of the values associated with selections in the list that are currently in the selected state.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged","title":"SelectedChanged class","text":"

    Bases: Generic[MessageSelectionType], Message

    Message sent when the collection of selected values changes.

    This message is sent when any change to the collection of selected values takes place; either by user interaction or by API calls.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.control","title":"control property","text":"
    control: SelectionList[MessageSelectionType]\n

    An alias for selection_list.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
    selection_list: SelectionList[MessageSelectionType]\n

    The SelectionList that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionHighlighted","title":"SelectionHighlighted class","text":"

    Bases: SelectionMessage

    Message sent when a selection is highlighted.

    Can be handled using on_selection_list_selection_highlighted in a subclass of SelectionList or in a parent node in the DOM.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage","title":"SelectionMessage class","text":"
    def __init__(self, selection_list, index):\n

    Bases: Generic[MessageSelectionType], Message

    Base class for all selection messages.

    Parameters Parameter Default Description selection_list SelectionList required

    The selection list that owns the selection.

    index int required

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.control","title":"control property","text":"
    control: OptionList\n

    The selection list that sent the message.

    This is an alias for SelectionMessage.selection_list and is used by the on decorator.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
    selection: Selection[MessageSelectionType] = (\n    selection_list.get_option_at_index(index)\n)\n

    The highlighted selection.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
    selection_index: int = index\n

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
    selection_list: SelectionList[MessageSelectionType] = (\n    selection_list\n)\n

    The selection list that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionToggled","title":"SelectionToggled class","text":"

    Bases: SelectionMessage

    Message sent when a selection is toggled.

    Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

    Note

    This message is only sent if the selection is toggled by user interaction. See SelectedChanged for a message sent when any change (selected or deselected, either by user interaction or by API calls) is made to the selected values.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_option","title":"add_option method","text":"
    def add_option(self, item=None):\n

    Add a new selection option to the end of the list.

    Parameters Parameter Default Description item NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool] None

    The new item to add.

    Returns Type Description Self

    The SelectionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    SelectionError

    If the selection option is of the wrong form.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_options","title":"add_options method","text":"
    def add_options(self, items):\n

    Add new selection options to the end of the list.

    Parameters Parameter Default Description items Iterable[NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]] required

    The new items to add.

    Returns Type Description Self

    The SelectionList instance.

    Raises Type Description DuplicateID

    If there is an attempt to use a duplicate ID.

    SelectionError

    If one of the selection options is of the wrong form.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.clear_options","title":"clear_options method","text":"
    def clear_options(self):\n

    Clear the content of the selection list.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect","title":"deselect method","text":"
    def deselect(self, selection):\n

    Mark the given selection as not selected.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to mark as not selected.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect_all","title":"deselect_all method","text":"
    def deselect_all(self):\n

    Deselect all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option","title":"get_option method","text":"
    def get_option(self, option_id):\n

    Get the selection option with the given ID.

    Parameters Parameter Default Description option_id str required

    The ID of the selection option to get.

    Returns Type Description Selection[SelectionType]

    The selection option with the ID.

    Raises Type Description OptionDoesNotExist

    If no selection option has the given ID.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option_at_index","title":"get_option_at_index method","text":"
    def get_option_at_index(self, index):\n

    Get the selection option at the given index.

    Parameters Parameter Default Description index int required

    The index of the selection option to get.

    Returns Type Description Selection[SelectionType]

    The selection option at that index.

    Raises Type Description OptionDoesNotExist

    If there is no selection option with the index.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select","title":"select method","text":"
    def select(self, selection):\n

    Mark the given selection as selected.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to mark as selected.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select_all","title":"select_all method","text":"
    def select_all(self):\n

    Select all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle","title":"toggle method","text":"
    def toggle(self, selection):\n

    Toggle the selected state of the given selection.

    Parameters Parameter Default Description selection Selection[SelectionType] | SelectionType required

    The selection to toggle.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle_all","title":"toggle_all method","text":"
    def toggle_all(self):\n

    Toggle all items.

    Returns Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.MessageSelectionType","title":"MessageSelectionType module-attribute","text":"
    MessageSelectionType = TypeVar('MessageSelectionType')\n

    The type for the value of a Selection in a SelectionList message.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionType","title":"SelectionType module-attribute","text":"
    SelectionType = TypeVar('SelectionType')\n

    The type for the value of a Selection in a SelectionList

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection","title":"Selection class","text":"
    def __init__(\n    self,\n    prompt,\n    value,\n    initial_state=False,\n    id=None,\n    disabled=False,\n):\n

    Bases: Generic[SelectionType], Option

    A selection for a SelectionList.

    Parameters Parameter Default Description prompt TextType required

    The prompt for the selection.

    value SelectionType required

    The value for the selection.

    initial_state bool False

    The initial selected state of the selection.

    id str | None None

    The optional ID for the selection.

    disabled bool False

    The initial enabled/disabled state. Enabled by default.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.initial_state","title":"initial_state property","text":"
    initial_state: bool\n

    The initial selected state for the selection.

    "},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.value","title":"value property","text":"
    value: SelectionType\n

    The value for this selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError class","text":"

    Bases: TypeError

    Type of an error raised if a selection is badly-formed.

    "},{"location":"widgets/sparkline/","title":"Sparkline","text":"

    Added in version 0.27.0

    A widget that is used to visually represent numerical data.

    • Focusable
    • Container
    "},{"location":"widgets/sparkline/#examples","title":"Examples","text":""},{"location":"widgets/sparkline/#basic-example","title":"Basic example","text":"

    The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.

    Tip

    The sparkline data is split into equally-sized chunks. Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.

    Outputsparkline_basic.pysparkline_basic.tcss

    SparklineBasicApp \u2582\u2584\u2588

    from textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\ndata = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2]  # (1)!\n\n\nclass SparklineBasicApp(App[None]):\n    CSS_PATH = \"sparkline_basic.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Sparkline(  # (2)!\n            data,  # (3)!\n            summary_function=max,  # (4)!\n        )\n\n\napp = SparklineBasicApp()\nif __name__ == \"__main__\":\n    app.run()\n
    1. We have 12 data points.
    2. This sparkline will have its width set to 3 via CSS.
    3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
    4. Each bar will represent its largest value. The largest value of each chunk is 2, 4, and 8, respectively. That explains why the first bar is half the height of the second and the second bar is half the height of the third.
    Screen {\n    align: center middle;\n}\n\nSparkline {\n    width: 3;  /* (1)! */\n    margin: 2;\n}\n
    1. By setting the width to 3 we get three buckets.
    "},{"location":"widgets/sparkline/#different-summary-functions","title":"Different summary functions","text":"

    The example below shows a sparkline widget with different summary functions. The summary function is what determines the height of each bar.

    Outputsparkline.pysparkline.tcss

    SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    import random\nfrom statistics import mean\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\nrandom.seed(73)\ndata = [random.expovariate(1 / 3) for _ in range(1000)]\n\n\nclass SparklineSummaryFunctionApp(App[None]):\n    CSS_PATH = \"sparkline.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Sparkline(data, summary_function=max)  # (1)!\n        yield Sparkline(data, summary_function=mean)  # (2)!\n        yield Sparkline(data, summary_function=min)  # (3)!\n\n\napp = SparklineSummaryFunctionApp()\nif __name__ == \"__main__\":\n    app.run()\n
    1. Each bar will show the largest value of that bucket.
    2. Each bar will show the mean value of that bucket.
    3. Each bar will show the smaller value of that bucket.
    Sparkline {\n    width: 100%;\n    margin: 2;\n}\n
    "},{"location":"widgets/sparkline/#changing-the-colors","title":"Changing the colors","text":"

    The example below shows how to use component classes to change the colors of the sparkline.

    Outputsparkline_colors.pysparkline_colors.tcss

    SparklineColorsApp \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582

    from math import sin\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\n\n\nclass SparklineColorsApp(App[None]):\n    CSS_PATH = \"sparkline_colors.tcss\"\n\n    def compose(self) -> ComposeResult:\n        nums = [abs(sin(x / 3.14)) for x in range(0, 360 * 6, 20)]\n        yield Sparkline(nums, summary_function=max, id=\"fst\")\n        yield Sparkline(nums, summary_function=max, id=\"snd\")\n        yield Sparkline(nums, summary_function=max, id=\"trd\")\n        yield Sparkline(nums, summary_function=max, id=\"frt\")\n        yield Sparkline(nums, summary_function=max, id=\"fft\")\n        yield Sparkline(nums, summary_function=max, id=\"sxt\")\n        yield Sparkline(nums, summary_function=max, id=\"svt\")\n        yield Sparkline(nums, summary_function=max, id=\"egt\")\n        yield Sparkline(nums, summary_function=max, id=\"nnt\")\n        yield Sparkline(nums, summary_function=max, id=\"tnt\")\n\n\napp = SparklineColorsApp()\nif __name__ == \"__main__\":\n    app.run()\n
    Sparkline {\n    width: 100%;\n    margin: 1;\n}\n\n#fst > .sparkline--max-color {\n    color: $success;\n}\n#fst > .sparkline--min-color {\n    color: $warning;\n}\n\n#snd > .sparkline--max-color {\n    color: $warning;\n}\n#snd > .sparkline--min-color {\n    color: $success;\n}\n\n#trd > .sparkline--max-color {\n    color: $error;\n}\n#trd > .sparkline--min-color {\n    color: $warning;\n}\n\n#frt > .sparkline--max-color {\n    color: $warning;\n}\n#frt > .sparkline--min-color {\n    color: $error;\n}\n\n#fft > .sparkline--max-color {\n    color: $accent;\n}\n#fft > .sparkline--min-color {\n    color: $accent 30%;\n}\n\n#sxt > .sparkline--max-color {\n    color: $accent 30%;\n}\n#sxt > .sparkline--min-color {\n    color: $accent;\n}\n\n#svt > .sparkline--max-color {\n    color: $error;\n}\n#svt > .sparkline--min-color {\n    color: $error 30%;\n}\n\n#egt > .sparkline--max-color {\n    color: $error 30%;\n}\n#egt > .sparkline--min-color {\n    color: $error;\n}\n\n#nnt > .sparkline--max-color {\n    color: $success;\n}\n#nnt > .sparkline--min-color {\n    color: $success 30%;\n}\n\n#tnt > .sparkline--max-color {\n    color: $success 30%;\n}\n#tnt > .sparkline--min-color {\n    color: $success;\n}\n
    "},{"location":"widgets/sparkline/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description data Sequence[float] | None None The data represented by the sparkline. summary_function Callable[[Sequence[float]], float] max The function that computes the height of each bar."},{"location":"widgets/sparkline/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/sparkline/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/sparkline/#component-classes","title":"Component Classes","text":"

    The sparkline widget provides the following component classes:

    Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

    Note

    These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

    Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets.Sparkline","title":"textual.widgets.Sparkline class","text":"
    def __init__(\n    self,\n    data=None,\n    *,\n    summary_function=None,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A sparkline widget to display numerical data.

    Parameters Parameter Default Description data Sequence[float] | None None

    The initial data to populate the sparkline with.

    summary_function Callable[[Sequence[float]], float] | None None

    Summarises bar values into a single value used to represent each bar.

    name str | None None

    The name of the widget.

    id str | None None

    The ID of the widget in the DOM.

    classes str | None None

    The CSS classes for the widget.

    disabled bool False

    Whether the widget is disabled or not.

    "},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"sparkline--max-color\",\n    \"sparkline--min-color\",\n}\n

    Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

    Note

    These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

    Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.data","title":"data class-attribute instance-attribute","text":"
    data = data\n

    The data that populates the sparkline.

    "},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.summary_function","title":"summary_function class-attribute instance-attribute","text":"
    summary_function = reactive[\n    Callable[[Sequence[float]], float]\n](_max_factory)\n

    The function that computes the value that represents each bar.

    "},{"location":"widgets/static/","title":"Static","text":"

    A widget which displays static content. Can be used for Rich renderables and can also be the base for other types of widgets.

    • Focusable
    • Container
    "},{"location":"widgets/static/#example","title":"Example","text":"

    The example below shows how you can use a Static widget as a simple text label (but see Label as a way of displaying text).

    Outputstatic.py

    StaticApp Hello,\u00a0world!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass StaticApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(\"Hello, world!\")\n\n\nif __name__ == \"__main__\":\n    app = StaticApp()\n    app.run()\n
    "},{"location":"widgets/static/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/static/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/static/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/static/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/static/#see-also","title":"See Also","text":"
    • Label
    • Pretty
    "},{"location":"widgets/static/#textual.widgets.Static","title":"textual.widgets.Static class","text":"
    def __init__(\n    self,\n    renderable=\"\",\n    *,\n    expand=False,\n    shrink=False,\n    markup=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A widget to display simple static content, or use as a base class for more complex widgets.

    Parameters Parameter Default Description renderable RenderableType ''

    A Rich renderable, or string containing console markup.

    expand bool False

    Expand content if required to fill container.

    shrink bool False

    Shrink content if required to fill container.

    markup bool True

    True if markup should be parsed and rendered.

    name str | None None

    Name of widget.

    id str | None None

    ID of Widget.

    classes str | None None

    Space separated list of class names.

    disabled bool False

    Whether the static is disabled or not.

    "},{"location":"widgets/static/#textual.widgets._static.Static.update","title":"update method","text":"
    def update(self, renderable=''):\n

    Update the widget's content area with new text or Rich renderable.

    Parameters Parameter Default Description renderable RenderableType ''

    A new rich renderable. Defaults to empty renderable;

    "},{"location":"widgets/switch/","title":"Switch","text":"

    A simple switch widget which stores a boolean value.

    • Focusable
    • Container
    "},{"location":"widgets/switch/#example","title":"Example","text":"

    The example below shows switches in various states.

    Outputswitch.pyswitch.tcss

    SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static, Switch\n\n\nclass SwitchApp(App):\n    def compose(self) -> ComposeResult:\n        yield Static(\"[b]Example switches\\n\", classes=\"label\")\n        yield Horizontal(\n            Static(\"off:     \", classes=\"label\"),\n            Switch(animate=False),\n            classes=\"container\",\n        )\n        yield Horizontal(\n            Static(\"on:      \", classes=\"label\"),\n            Switch(value=True),\n            classes=\"container\",\n        )\n\n        focused_switch = Switch()\n        focused_switch.focus()\n        yield Horizontal(\n            Static(\"focused: \", classes=\"label\"), focused_switch, classes=\"container\"\n        )\n\n        yield Horizontal(\n            Static(\"custom:  \", classes=\"label\"),\n            Switch(id=\"custom-design\"),\n            classes=\"container\",\n        )\n\n\napp = SwitchApp(css_path=\"switch.tcss\")\nif __name__ == \"__main__\":\n    app.run()\n
    Screen {\n    align: center middle;\n}\n\n.container {\n    height: auto;\n    width: auto;\n}\n\nSwitch {\n    height: auto;\n    width: auto;\n}\n\n.label {\n    height: 3;\n    content-align: center middle;\n    width: auto;\n}\n\n#custom-design {\n    background: darkslategrey;\n}\n\n#custom-design > .switch--slider {\n    color: dodgerblue;\n    background: darkslateblue;\n}\n
    "},{"location":"widgets/switch/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the switch."},{"location":"widgets/switch/#messages","title":"Messages","text":"
    • Switch.Changed
    "},{"location":"widgets/switch/#bindings","title":"Bindings","text":"

    The switch widget defines the following bindings:

    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#component-classes","title":"Component Classes","text":"

    The switch widget provides the following component classes:

    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#additional-notes","title":"Additional Notes","text":"
    • To remove the spacing around a Switch, set border: none; and padding: 0;.
    "},{"location":"widgets/switch/#textual.widgets.Switch","title":"textual.widgets.Switch class","text":"
    def __init__(\n    self,\n    value=False,\n    *,\n    animate=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A switch widget that represents a boolean value.

    Can be toggled by clicking on it or through its bindings.

    The switch widget also contains component classes that enable more customization.

    Parameters Parameter Default Description value bool False

    The initial value of the switch.

    animate bool True

    True if the switch should animate when toggled.

    name str | None None

    The name of the switch.

    id str | None None

    The ID of the switch in the DOM.

    classes str | None None

    The CSS classes of the switch.

    disabled bool False

    Whether the switch is disabled or not.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter,space\", \"toggle\", \"Toggle\", show=False)\n]\n
    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets._switch.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {'switch--slider'}\n
    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets._switch.Switch.slider_pos","title":"slider_pos class-attribute instance-attribute","text":"
    slider_pos = reactive(0.0)\n

    The position of the slider.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.value","title":"value class-attribute instance-attribute","text":"
    value = reactive(False, init=False)\n

    The value of the switch; True for on and False for off.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed","title":"Changed class","text":"
    def __init__(self, switch, value):\n

    Bases: Message

    Posted when the status of the switch changes.

    Can be handled using on_switch_changed in a subclass of Switch or in a parent widget in the DOM.

    Attributes Name Type Description value bool

    The value that the switch was changed to.

    switch Switch

    The Switch widget that was changed.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed.control","title":"control property","text":"
    control: Switch\n

    Alias for self.switch.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.action_toggle","title":"action_toggle method","text":"
    def action_toggle(self):\n

    Toggle the state of the switch.

    "},{"location":"widgets/switch/#textual.widgets._switch.Switch.toggle","title":"toggle method","text":"
    def toggle(self):\n

    Toggle the switch value.

    As a result of the value changing, a Switch.Changed message will be posted.

    Returns Type Description Self

    The Switch instance.

    "},{"location":"widgets/tabbed_content/","title":"TabbedContent","text":"

    Added in version 0.16.0

    Switch between mutually exclusive content panes via a row of tabs.

    • Focusable
    • Container

    This widget combines the Tabs and ContentSwitcher widgets to create a convenient way of navigating content.

    Only a single child of TabbedContent is visible at once. Each child has an associated tab which will make it visible and hide the others.

    "},{"location":"widgets/tabbed_content/#composing","title":"Composing","text":"

    There are two ways to provide the titles for the tab. You can pass them as positional arguments to the TabbedContent constructor:

    def compose(self) -> ComposeResult:\n    with TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\n        yield Markdown(LETO)\n        yield Markdown(JESSICA)\n        yield Markdown(PAUL)\n

    Alternatively you can wrap the content in a TabPane widget, which takes the tab title as the first parameter:

    def compose(self) -> ComposeResult:\n    with TabbedContent():\n        with TabPane(\"Leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\"):\n            yield Markdown(PAUL)\n
    "},{"location":"widgets/tabbed_content/#switching-tabs","title":"Switching tabs","text":"

    If you need to programmatically switch tabs, you should provide an id attribute to the TabPanes.

    def compose(self) -> ComposeResult:\n    with TabbedContent():\n        with TabPane(\"Leto\", id=\"leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\", id=\"jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\", id=\"paul\"):\n            yield Markdown(PAUL)\n

    You can then switch tabs by setting the active reactive attribute:

    # Switch to Jessica tab\nself.query_one(TabbedContent).active = \"jessica\"\n

    Note

    If you don't provide id attributes to the tab panes, they will be assigned sequentially starting at tab-1 (then tab-2 etc).

    "},{"location":"widgets/tabbed_content/#initial-tab","title":"Initial tab","text":"

    The first child of TabbedContent will be the initial active tab by default. You can pick a different initial tab by setting the initial argument to the id of the tab:

    def compose(self) -> ComposeResult:\n    with TabbedContent(initial=\"jessica\"):\n        with TabPane(\"Leto\", id=\"leto\"):\n            yield Markdown(LETO)\n        with TabPane(\"Jessica\", id=\"jessica\"):\n            yield Markdown(JESSICA)\n        with TabPane(\"Paul\", id=\"paul\"):\n            yield Markdown(PAUL)\n
    "},{"location":"widgets/tabbed_content/#example","title":"Example","text":"

    The following example contains a TabbedContent with three tabs.

    Outputtabbed_content.py

    TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Markdown, TabbedContent, TabPane\n\nLETO = \"\"\"\n# Duke Leto I Atreides\n\nHead of House Atreides.\n\"\"\"\n\nJESSICA = \"\"\"\n# Lady Jessica\n\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\n\nPAUL = \"\"\"\n# Paul Atreides\n\nSon of Leto and Jessica.\n\"\"\"\n\n\nclass TabbedApp(App):\n    \"\"\"An example of tabbed content.\"\"\"\n\n    BINDINGS = [\n        (\"l\", \"show_tab('leto')\", \"Leto\"),\n        (\"j\", \"show_tab('jessica')\", \"Jessica\"),\n        (\"p\", \"show_tab('paul')\", \"Paul\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Compose app with tabbed content.\"\"\"\n        # Footer to show keys\n        yield Footer()\n\n        # Add the TabbedContent widget\n        with TabbedContent(initial=\"jessica\"):\n            with TabPane(\"Leto\", id=\"leto\"):  # First tab\n                yield Markdown(LETO)  # Tab content\n            with TabPane(\"Jessica\", id=\"jessica\"):\n                yield Markdown(JESSICA)\n                with TabbedContent(\"Paul\", \"Alia\"):\n                    yield TabPane(\"Paul\", Label(\"First child\"))\n                    yield TabPane(\"Alia\", Label(\"Second child\"))\n\n            with TabPane(\"Paul\", id=\"paul\"):\n                yield Markdown(PAUL)\n\n    def action_show_tab(self, tab: str) -> None:\n        \"\"\"Switch to a new tab.\"\"\"\n        self.get_child_by_type(TabbedContent).active = tab\n\n\nif __name__ == \"__main__\":\n    app = TabbedApp()\n    app.run()\n
    "},{"location":"widgets/tabbed_content/#styling","title":"Styling","text":"

    The TabbedContent widget is composed of two main sub-widgets: a Tabs and a ContentSwitcher; you can style them accordingly.

    The tabs within the Tabs widget will have prefixed IDs; each ID being the ID of the TabPane the Tab is for, prefixed with --content-tab-. If you wish to style individual tabs within the TabbedContent widget you will need to use that prefix for the Tab IDs.

    For example, to create a TabbedContent that has red and green labels:

    Outputtabbed_content.py

    ColorTabsApp RedGreen \u2501\u2578\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Red!

    from textual.app import App, ComposeResult\nfrom textual.widgets import Label, TabbedContent, TabPane\n\n\nclass ColorTabsApp(App):\n    CSS = \"\"\"\n    TabbedContent #--content-tab-green {\n        color: green;\n    }\n\n    TabbedContent #--content-tab-red {\n        color: red;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        with TabbedContent():\n            with TabPane(\"Red\", id=\"red\"):\n                yield Label(\"Red!\")\n            with TabPane(\"Green\", id=\"green\"):\n                yield Label(\"Green!\")\n\n\nif __name__ == \"__main__\":\n    ColorTabsApp().run()\n
    "},{"location":"widgets/tabbed_content/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The id attribute of the active tab. Set this to switch tabs."},{"location":"widgets/tabbed_content/#messages","title":"Messages","text":"
    • TabbedContent.TabActivated
    "},{"location":"widgets/tabbed_content/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/tabbed_content/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/tabbed_content/#see-also","title":"See also","text":"
    • Tabs
    • ContentSwitcher
    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent","title":"textual.widgets.TabbedContent class","text":"
    def __init__(\n    self,\n    *titles,\n    initial=\"\",\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A container with associated tabs to toggle content visibility.

    Parameters Parameter Default Description *titles TextType ()

    Positional argument will be used as title.

    initial str ''

    The id of the initial tab, or empty string to select the first tab.

    name str | None None

    The name of the button.

    id str | None None

    The ID of the button in the DOM.

    classes str | None None

    The CSS classes of the button.

    disabled bool False

    Whether the button is disabled or not.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.active","title":"active class-attribute instance-attribute","text":"
    active: reactive[str] = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.active_pane","title":"active_pane property","text":"
    active_pane: TabPane | None\n

    The currently active pane, or None if no pane is active.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.tab_count","title":"tab_count property","text":"
    tab_count: int\n

    Total number of tabs.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared","title":"Cleared class","text":"
    def __init__(self, tabbed_content):\n

    Bases: Message

    Posted when there are no more tab panes.

    Parameters Parameter Default Description tabbed_content TabbedContent required

    The TabbedContent widget.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.control","title":"control property","text":"
    control: TabbedContent\n

    The TabbedContent widget that was cleared of all tab panes.

    This is an alias for Cleared.tabbed_content and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.tabbed_content","title":"tabbed_content instance-attribute","text":"
    tabbed_content = tabbed_content\n

    The TabbedContent widget that contains the tab activated.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated","title":"TabActivated class","text":"
    def __init__(self, tabbed_content, tab):\n

    Bases: Message

    Posted when the active tab changes.

    Parameters Parameter Default Description tabbed_content TabbedContent required

    The TabbedContent widget.

    tab ContentTab required

    The Tab widget that was selected (contains the tab label).

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'pane'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.control","title":"control property","text":"
    control: TabbedContent\n

    The TabbedContent widget that contains the tab activated.

    This is an alias for TabActivated.tabbed_content and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.pane","title":"pane instance-attribute","text":"
    pane = tabbed_content.get_pane(tab)\n

    The TabPane widget that was activated by selecting the tab.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tab","title":"tab instance-attribute","text":"
    tab = tab\n

    The Tab widget that was selected (contains the tab label).

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tabbed_content","title":"tabbed_content instance-attribute","text":"
    tabbed_content = tabbed_content\n

    The TabbedContent widget that contains the tab activated.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.add_pane","title":"add_pane method","text":"
    def add_pane(self, pane, *, before=None, after=None):\n

    Add a new pane to the tabbed content.

    Parameters Parameter Default Description pane TabPane required

    The pane to add.

    before TabPane | str | None None

    Optional pane or pane ID to add the pane before.

    after TabPane | str | None None

    Optional pane or pane ID to add the pane after.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be added.

    Raises Type Description Tabs.TabError

    If there is a problem with the addition request.

    Note

    Only one of before or after can be provided. If both are provided an exception is raised.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.clear_panes","title":"clear_panes method","text":"
    def clear_panes(self):\n

    Remove all the panes in the tabbed content.

    Returns Type Description AwaitComplete

    An optionally awaitable object which waits for all panes to be removed and the Cleared message to be posted.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.disable_tab","title":"disable_tab method","text":"
    def disable_tab(self, tab_id):\n

    Disables the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to disable.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.enable_tab","title":"enable_tab method","text":"
    def enable_tab(self, tab_id):\n

    Enables the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to enable.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.get_pane","title":"get_pane method","text":"
    def get_pane(self, pane_id):\n

    Get the TabPane associated with the given ID or tab.

    Parameters Parameter Default Description pane_id str | ContentTab required

    The ID of the pane to get, or the Tab it is associated with.

    Returns Type Description TabPane

    The TabPane associated with the ID or the given tab.

    Raises Type Description ValueError

    Raised if no ID was available.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.get_tab","title":"get_tab method","text":"
    def get_tab(self, pane_id):\n

    Get the Tab associated with the given ID or TabPane.

    Parameters Parameter Default Description pane_id str | TabPane required

    The ID of the pane, or the pane itself.

    Returns Type Description Tab

    The Tab associated with the ID.

    Raises Type Description ValueError

    Raised if no ID was available.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.hide_tab","title":"hide_tab method","text":"
    def hide_tab(self, tab_id):\n

    Hides the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to hide.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.remove_pane","title":"remove_pane method","text":"
    def remove_pane(self, pane_id):\n

    Remove a given pane from the tabbed content.

    Parameters Parameter Default Description pane_id str required

    The ID of the pane to remove.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be removed and the Cleared message to be posted.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.show_tab","title":"show_tab method","text":"
    def show_tab(self, tab_id):\n

    Shows the tab with the given ID.

    Parameters Parameter Default Description tab_id str required

    The ID of the TabPane to show.

    Raises Type Description Tabs.TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.validate_active","title":"validate_active method","text":"
    def validate_active(self, active):\n

    It doesn't make sense for active to be an empty string.

    Parameters Parameter Default Description active str required

    Attribute to be validated.

    Returns Type Description str

    Value of active.

    Raises Type Description ValueError

    If the active attribute is set to empty string when there are tabs available.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane","title":"textual.widgets.TabPane class","text":"
    def __init__(\n    self,\n    title,\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A container for switchable content, with additional title.

    This widget is intended to be used with TabbedContent.

    Parameters Parameter Default Description title TextType required

    Title of the TabPane (will be displayed in a tab label).

    *children Widget ()

    Widget to go inside the TabPane.

    name str | None None

    Optional name for the TabPane.

    id str | None None

    Optional ID for the TabPane.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the TabPane is disabled or not.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Disabled","title":"Disabled class","text":"

    Bases: TabPaneMessage

    Sent when a tab pane is disabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Enabled","title":"Enabled class","text":"

    Bases: TabPaneMessage

    Sent when a tab pane is enabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage","title":"TabPaneMessage class","text":"

    Bases: Message

    Base class for TabPane messages.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.control","title":"control property","text":"
    control: TabPane\n

    The tab pane that is the object of this message.

    This is an alias for the attribute tab_pane and is used by the on decorator.

    "},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
    tab_pane: TabPane\n

    The TabPane that is he object of this message.

    "},{"location":"widgets/tabs/","title":"Tabs","text":"

    Added in version 0.15.0

    Displays a number of tab headers which may be activated with a click or navigated with cursor keys.

    • Focusable
    • Container

    Construct a Tabs widget with strings or Text objects as positional arguments, which will set the labels in the tabs. Here's an example with three tabs:

    def compose(self) -> ComposeResult:\n    yield Tabs(\"First tab\", \"Second tab\", Text.from_markup(\"[u]Third[/u] tab\"))\n

    This will create Tab widgets internally, with auto-incrementing id attributes (\"tab-1\", \"tab-2\" etc). You can also supply Tab objects directly in the constructor, which will allow you to explicitly set an id. Here's an example:

    def compose(self) -> ComposeResult:\n    yield Tabs(\n        Tab(\"First tab\", id=\"one\"),\n        Tab(\"Second tab\", id=\"two\"),\n    )\n

    When the user switches to a tab by clicking or pressing keys, then Tabs will send a Tabs.TabActivated message which contains the tab that was activated. You can then use event.tab.id attribute to perform any related actions.

    "},{"location":"widgets/tabs/#clearing-tabs","title":"Clearing tabs","text":"

    Clear tabs by calling the clear method. Clearing the tabs will send a Tabs.TabActivated message with the tab attribute set to None.

    "},{"location":"widgets/tabs/#adding-tabs","title":"Adding tabs","text":"

    Tabs may be added dynamically with the add_tab method, which accepts strings, Text, or Tab objects.

    "},{"location":"widgets/tabs/#example","title":"Example","text":"

    The following example adds a Tabs widget above a text label. Press A to add a tab, C to clear the tabs.

    Outputtabs.py

    TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

    from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Tabs\n\nNAMES = [\n    \"Paul Atreidies\",\n    \"Duke Leto Atreides\",\n    \"Lady Jessica\",\n    \"Gurney Halleck\",\n    \"Baron Vladimir Harkonnen\",\n    \"Glossu Rabban\",\n    \"Chani\",\n    \"Silgar\",\n]\n\n\nclass TabsApp(App):\n    \"\"\"Demonstrates the Tabs widget.\"\"\"\n\n    CSS = \"\"\"\n    Tabs {\n        dock: top;\n    }\n    Screen {\n        align: center middle;\n    }\n    Label {\n        margin:1 1;\n        width: 100%;\n        height: 100%;\n        background: $panel;\n        border: tall $primary;\n        content-align: center middle;\n    }\n    \"\"\"\n\n    BINDINGS = [\n        (\"a\", \"add\", \"Add tab\"),\n        (\"r\", \"remove\", \"Remove active tab\"),\n        (\"c\", \"clear\", \"Clear tabs\"),\n    ]\n\n    def compose(self) -> ComposeResult:\n        yield Tabs(NAMES[0])\n        yield Label()\n        yield Footer()\n\n    def on_mount(self) -> None:\n        \"\"\"Focus the tabs when the app starts.\"\"\"\n        self.query_one(Tabs).focus()\n\n    def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:\n        \"\"\"Handle TabActivated message sent by Tabs.\"\"\"\n        label = self.query_one(Label)\n        if event.tab is None:\n            # When the tabs are cleared, event.tab will be None\n            label.visible = False\n        else:\n            label.visible = True\n            label.update(event.tab.label)\n\n    def action_add(self) -> None:\n        \"\"\"Add a new tab.\"\"\"\n        tabs = self.query_one(Tabs)\n        # Cycle the names\n        NAMES[:] = [*NAMES[1:], NAMES[0]]\n        tabs.add_tab(NAMES[0])\n\n    def action_remove(self) -> None:\n        \"\"\"Remove active tab.\"\"\"\n        tabs = self.query_one(Tabs)\n        active_tab = tabs.active_tab\n        if active_tab is not None:\n            tabs.remove_tab(active_tab.id)\n\n    def action_clear(self) -> None:\n        \"\"\"Clear the tabs.\"\"\"\n        self.query_one(Tabs).clear()\n\n\nif __name__ == \"__main__\":\n    app = TabsApp()\n    app.run()\n
    "},{"location":"widgets/tabs/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The ID of the active tab. Set this attribute to a tab ID to change the active tab."},{"location":"widgets/tabs/#messages","title":"Messages","text":"
    • Tabs.TabActivated
    • Tabs.Cleared
    "},{"location":"widgets/tabs/#bindings","title":"Bindings","text":"

    The Tabs widget defines the following bindings:

    Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#component-classes","title":"Component Classes","text":"

    This widget has no component classes.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs","title":"textual.widgets.Tabs class","text":"
    def __init__(\n    self,\n    *tabs,\n    active=None,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Widget

    A row of tabs.

    Parameters Parameter Default Description *tabs Tab | TextType ()

    Positional argument should be explicit Tab objects, or a str or Text.

    active str | None None

    ID of the tab which should be active on start.

    name str | None None

    Optional name for the input widget.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Optional initial classes for the widget.

    disabled bool False

    Whether the input is disabled or not.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\n        \"left\", \"previous_tab\", \"Previous tab\", show=False\n    ),\n    Binding(\"right\", \"next_tab\", \"Next tab\", show=False),\n]\n
    Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active","title":"active class-attribute instance-attribute","text":"
    active: reactive[str] = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active_tab","title":"active_tab property","text":"
    active_tab: Tab | None\n

    The currently active tab, or None if there are no active tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.tab_count","title":"tab_count property","text":"
    tab_count: int\n

    Total number of tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared","title":"Cleared class","text":"
    def __init__(self, tabs):\n

    Bases: Message

    Sent when there are no active tabs.

    This can occur when Tabs are cleared, or if all tabs are hidden.

    Parameters Parameter Default Description tabs Tabs required

    The tabs widget.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.control","title":"control property","text":"
    control: Tabs\n

    The tabs widget which was cleared.

    This is an alias for Cleared.tabs which is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.tabs","title":"tabs instance-attribute","text":"
    tabs: Tabs = tabs\n

    The tabs widget which was cleared.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabActivated","title":"TabActivated class","text":"

    Bases: TabMessage

    Sent when a new tab is activated.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabDisabled","title":"TabDisabled class","text":"

    Bases: TabMessage

    Sent when a tab is disabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabEnabled","title":"TabEnabled class","text":"

    Bases: TabMessage

    Sent when a tab is enabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabError","title":"TabError class","text":"

    Bases: Exception

    Exception raised when there is an error relating to tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabHidden","title":"TabHidden class","text":"

    Bases: TabMessage

    Sent when a tab is hidden.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage","title":"TabMessage class","text":"
    def __init__(self, tabs, tab):\n

    Bases: Message

    Parent class for all messages that have to do with a specific tab.

    Parameters Parameter Default Description tabs Tabs required

    The Tabs widget.

    tab Tab required

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
    ALLOW_SELECTOR_MATCH = {'tab'}\n

    Additional message attributes that can be used with the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.control","title":"control property","text":"
    control: Tabs\n

    The tabs widget containing the tab that is the object of this message.

    This is an alias for the attribute tabs and is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tab","title":"tab instance-attribute","text":"
    tab: Tab = tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
    tabs: Tabs = tabs\n

    The tabs widget containing the tab.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabShown","title":"TabShown class","text":"

    Bases: TabMessage

    Sent when a tab is shown.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_next_tab","title":"action_next_tab method","text":"
    def action_next_tab(self):\n

    Make the next tab active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_previous_tab","title":"action_previous_tab method","text":"
    def action_previous_tab(self):\n

    Make the previous tab active.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.add_tab","title":"add_tab method","text":"
    def add_tab(self, tab, *, before=None, after=None):\n

    Add a new tab to the end of the tab list.

    Parameters Parameter Default Description tab Tab | str | Text required

    A new tab object, or a label (str or Text).

    before Tab | str | None None

    Optional tab or tab ID to add the tab before.

    after Tab | str | None None

    Optional tab or tab ID to add the tab after.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be mounted and internal state to be fully updated to reflect the new tab.

    Raises Type Description Tabs.TabError

    If there is a problem with the addition request.

    Note

    Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all the tabs.

    Returns Type Description AwaitComplete

    An awaitable object that waits for the tabs to be removed.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.disable","title":"disable method","text":"
    def disable(self, tab_id):\n

    Disable the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to disable.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.enable","title":"enable method","text":"
    def enable(self, tab_id):\n

    Enable the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to enable.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.hide","title":"hide method","text":"
    def hide(self, tab_id):\n

    Hide the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to hide.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.remove_tab","title":"remove_tab method","text":"
    def remove_tab(self, tab_or_id):\n

    Remove a tab.

    Parameters Parameter Default Description tab_or_id Tab | str | None required

    The Tab to remove or its id.

    Returns Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be removed.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.show","title":"show method","text":"
    def show(self, tab_id):\n

    Show the indicated tab.

    Parameters Parameter Default Description tab_id str required

    The ID of the Tab to show.

    Returns Type Description Tab

    The Tab that was targeted.

    Raises Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.validate_active","title":"validate_active method","text":"
    def validate_active(self, active):\n

    Check id assigned to active attribute is a valid tab.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.watch_active","title":"watch_active method","text":"
    def watch_active(self, previously_active, active):\n

    Handle a change to the active tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab","title":"textual.widgets.Tab class","text":"
    def __init__(\n    self, label, *, id=None, classes=None, disabled=False\n):\n

    Bases: Static

    A Widget to manage a single tab within a Tabs widget.

    Parameters Parameter Default Description label TextType required

    The label to use in the tab.

    id str | None None

    Optional ID for the widget.

    classes str | None None

    Space separated list of class names.

    disabled bool False

    Whether the tab is disabled or not.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.label","title":"label writable property","text":"
    label: Text\n

    The label for the tab.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.label_text","title":"label_text property","text":"
    label_text: str\n

    Undecorated text of the label.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Clicked","title":"Clicked class","text":"

    Bases: TabMessage

    A tab was clicked.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Disabled","title":"Disabled class","text":"

    Bases: TabMessage

    A tab was disabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Enabled","title":"Enabled class","text":"

    Bases: TabMessage

    A tab was enabled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Relabelled","title":"Relabelled class","text":"

    Bases: TabMessage

    A tab was relabelled.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage","title":"TabMessage class","text":"

    Bases: Message

    Tab-related messages.

    These are mostly intended for internal use when interacting with Tabs.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.control","title":"control property","text":"
    control: Tab\n

    The tab that is the object of this message.

    This is an alias for the attribute tab and is used by the on decorator.

    "},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
    tab: Tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/text_area/","title":"TextArea","text":"

    Tip

    Added in version 0.38.0. Soft wrapping added in version 0.48.0.

    A widget for editing text which may span multiple lines. Supports text selection, soft wrapping, optional syntax highlighting with tree-sitter and a variety of keybindings.

    • Focusable
    • Container
    "},{"location":"widgets/text_area/#guide","title":"Guide","text":""},{"location":"widgets/text_area/#code-editing-vs-plain-text-editing","title":"Code editing vs plain text editing","text":"

    By default, the TextArea widget is a standard multi-line input box with soft-wrapping enabled.

    If you're interested in editing code, you may wish to use the [TextArea.code_editor] convenience constructor. This is a method which, by default, returns a new TextArea with soft-wrapping disabled, line numbers enabled, and the tab key behavior configured to insert \\t.

    "},{"location":"widgets/text_area/#syntax-highlighting-dependencies","title":"Syntax highlighting dependencies","text":"

    To enable syntax highlighting, you'll need to install the syntax extra dependencies:

    pippoetry
    pip install \"textual[syntax]\"\n
    poetry add \"textual[syntax]\"\n

    This will install tree-sitter and tree-sitter-languages. These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not available. After installing, you can set the language reactive attribute on the TextArea to enable highlighting.

    "},{"location":"widgets/text_area/#loading-text","title":"Loading text","text":"

    In this example we load some initial text into the TextArea, and set the language to \"python\" to enable syntax highlighting.

    Outputtext_area_example.py

    TextAreaExample \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adef\u00a0hello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258adef\u00a0goodbye(name):\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\n\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\n\n\nclass TextAreaExample(App):\n    def compose(self) -> ComposeResult:\n        yield TextArea(TEXT, language=\"python\")\n\n\napp = TextAreaExample()\nif __name__ == \"__main__\":\n    app.run()\n

    To update the content programmatically, set the text property to a string value.

    To update the parser used for syntax highlighting, set the language reactive attribute:

    # Set the language to Markdown\ntext_area.language = \"markdown\"\n

    Note

    More built-in languages will be added in the future. For now, you can add your own.

    "},{"location":"widgets/text_area/#reading-content-from-textarea","title":"Reading content from TextArea","text":"

    There are a number of ways to retrieve content from the TextArea:

    • The TextArea.text property returns all content in the text area as a string.
    • The TextArea.selected_text property returns the text corresponding to the current selection.
    • The TextArea.get_text_range method returns the text between two locations.

    In all cases, when multiple lines of text are retrieved, the document line separator will be used.

    "},{"location":"widgets/text_area/#editing-content-inside-textarea","title":"Editing content inside TextArea","text":"

    The content of the TextArea can be updated using the replace method. This method is the programmatic equivalent of selecting some text and then pasting.

    Some other convenient methods are available, such as insert, delete, and clear.

    "},{"location":"widgets/text_area/#working-with-the-cursor","title":"Working with the cursor","text":""},{"location":"widgets/text_area/#moving-the-cursor","title":"Moving the cursor","text":"

    The cursor location is available via the cursor_location property, which represents the location of the cursor as a tuple (row_index, column_index). These indices are zero-based and represent the position of the cursor in the content. Writing a new value to cursor_location will immediately update the location of the cursor.

    >>> text_area = TextArea()\n>>> text_area.cursor_location\n(0, 0)\n>>> text_area.cursor_location = (0, 4)\n>>> text_area.cursor_location\n(0, 4)\n

    cursor_location is a simple way to move the cursor programmatically, but it doesn't let us select text.

    "},{"location":"widgets/text_area/#selecting-text","title":"Selecting text","text":"

    To select text, we can use the selection reactive attribute. Let's select the first two lines of text in a document by adding text_area.selection = Selection(start=(0, 0), end=(2, 0)) to our code:

    Outputtext_area_selection.py

    TextAreaSelection \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adef\u00a0hello(name):\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u258e \u258a\u258e \u258adef\u00a0goodbye(name):\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nfrom textual.widgets.text_area import Selection\n\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\n\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\n\n\nclass TextAreaSelection(App):\n    def compose(self) -> ComposeResult:\n        text_area = TextArea(TEXT, language=\"python\")\n        text_area.selection = Selection(start=(0, 0), end=(2, 0))  # (1)!\n        yield text_area\n\n\napp = TextAreaSelection()\nif __name__ == \"__main__\":\n    app.run()\n
    1. Selects the first two lines of text.

    Note that selections can happen in both directions, so Selection((2, 0), (0, 0)) is also valid.

    Tip

    The end attribute of the selection is always equal to TextArea.cursor_location. In other words, the cursor_location attribute is simply a convenience for accessing text_area.selection.end.

    "},{"location":"widgets/text_area/#more-cursor-utilities","title":"More cursor utilities","text":"

    There are a number of additional utility methods available for interacting with the cursor.

    "},{"location":"widgets/text_area/#location-information","title":"Location information","text":"

    A number of properties exist on TextArea which give information about the current cursor location. These properties begin with cursor_at_, and return booleans. For example, cursor_at_start_of_line tells us if the cursor is at a start of line.

    We can also check the location the cursor would arrive at if we were to move it. For example, get_cursor_right_location returns the location the cursor would move to if it were to move right. A number of similar methods exist, with names like get_cursor_*_location.

    "},{"location":"widgets/text_area/#cursor-movement-methods","title":"Cursor movement methods","text":"

    The move_cursor method allows you to move the cursor to a new location while selecting text, or move the cursor and scroll to keep it centered.

    # Move the cursor from its current location to row index 4,\n# column index 8, while selecting all the text between.\ntext_area.move_cursor((4, 8), select=True)\n

    The move_cursor_relative method offers a very similar interface, but moves the cursor relative to its current location.

    "},{"location":"widgets/text_area/#common-selections","title":"Common selections","text":"

    There are some methods available which make common selections easier:

    • select_line selects a line by index. Bound to F6 by default.
    • select_all selects all text. Bound to F7 by default.
    "},{"location":"widgets/text_area/#themes","title":"Themes","text":"

    TextArea ships with some builtin themes, and you can easily add your own.

    Themes give you control over the look and feel, including syntax highlighting, the cursor, selection, gutter, and more.

    "},{"location":"widgets/text_area/#default-theme","title":"Default theme","text":"

    The default TextArea theme is called css. This a theme which takes values entirely from CSS. This means that the default appearance of the widget fits nicely into a standard Textual application, and looks right on both dark and light mode.

    More complex applications such as code editors will likely want to use pre-defined themes such as monokai. This involves using a TextAreaTheme object, which we cover in detail below. This allows full customization of the TextArea, including syntax highlighting, at the code level.

    "},{"location":"widgets/text_area/#using-builtin-themes","title":"Using builtin themes","text":"

    The initial theme of the TextArea is determined by the theme parameter.

    # Create a TextArea with the 'dracula' theme.\nyield TextArea(\"print(123)\", language=\"python\", theme=\"dracula\")\n

    You can check which themes are available using the available_themes property.

    >>> text_area = TextArea()\n>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark'}\n

    After creating a TextArea, you can change the theme by setting the theme attribute to one of the available themes.

    text_area.theme = \"vscode_dark\"\n

    On setting this attribute the TextArea will immediately refresh to display the updated theme.

    "},{"location":"widgets/text_area/#custom-themes","title":"Custom themes","text":"

    Using custom (non-builtin) themes is two-step process:

    1. Create an instance of TextAreaTheme.
    2. Register it using TextArea.register_theme.
    "},{"location":"widgets/text_area/#1-creating-a-theme","title":"1. Creating a theme","text":"

    Let's create a simple theme, \"my_cool_theme\", which colors the cursor blue, and the cursor line yellow. Our theme will also syntax highlight strings as red, and comments as magenta.

    from rich.style import Style\nfrom textual.widgets.text_area import TextAreaTheme\n# ...\nmy_theme = TextAreaTheme(\n    # This name will be used to refer to the theme...\n    name=\"my_cool_theme\",\n    # Basic styles such as background, cursor, selection, gutter, etc...\n    cursor_style=Style(color=\"white\", bgcolor=\"blue\"),\n    cursor_line_style=Style(bgcolor=\"yellow\"),\n    # `syntax_styles` is for syntax highlighting.\n    # It maps tokens parsed from the document to Rich styles.\n    syntax_styles={\n        \"string\": Style(color=\"red\"),\n        \"comment\": Style(color=\"magenta\"),\n    }\n)\n

    Attributes like cursor_style and cursor_line_style apply general language-agnostic styling to the widget. If you choose not to supply a value for one of these attributes, it will be taken from the CSS component styles.

    The syntax_styles attribute of TextAreaTheme is used for syntax highlighting and depends on the language currently in use. For more details, see syntax highlighting.

    If you wish to build on an existing theme, you can obtain a reference to it using the TextAreaTheme.get_builtin_theme classmethod:

    from textual.widgets.text_area import TextAreaTheme\n\nmonokai = TextAreaTheme.get_builtin_theme(\"monokai\")\n
    "},{"location":"widgets/text_area/#2-registering-a-theme","title":"2. Registering a theme","text":"

    Our theme can now be registered with the TextArea instance.

    text_area.register_theme(my_theme)\n

    After registering a theme, it'll appear in the available_themes:

    >>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}\n

    We can now switch to it:

    text_area.theme = \"my_cool_theme\"\n

    This immediately updates the appearance of the TextArea:

    TextAreaCustomThemes \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a#\u00a0says\u00a0hello\u258e \u258adef\u00a0hello(name):\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u258e \u258a\u258e \u258a#\u00a0says\u00a0goodbye\u2584\u2584\u258e \u258adef\u00a0goodbye(name):\u00a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widgets/text_area/#tab-behaviour","title":"Tab behaviour","text":"

    Pressing the Tab key will shift focus to the next widget in your application by default. This matches how other widgets work in Textual. To have Tab insert a \\t character, set the tab_behaviour attribute to the string value \"indent\".

    "},{"location":"widgets/text_area/#indentation","title":"Indentation","text":"

    The character(s) inserted when you press tab is controlled by setting the indent_type attribute to either tabs or spaces.

    If indent_type == \"spaces\", pressing Tab will insert up to indent_width spaces in order to align with the next tab stop.

    "},{"location":"widgets/text_area/#line-separators","title":"Line separators","text":"

    When content is loaded into TextArea, the content is scanned from beginning to end and the first occurrence of a line separator is recorded.

    This separator will then be used when content is later read from the TextArea via the text property. The TextArea widget does not support exporting text which contains mixed line endings.

    Similarly, newline characters pasted into the TextArea will be converted.

    You can check the line separator of the current document by inspecting TextArea.document.newline:

    >>> text_area = TextArea()\n>>> text_area.document.newline\n'\\n'\n
    "},{"location":"widgets/text_area/#line-numbers","title":"Line numbers","text":"

    The gutter (column on the left containing line numbers) can be toggled by setting the show_line_numbers attribute to True or False.

    Setting this attribute will immediately repaint the TextArea to reflect the new value.

    "},{"location":"widgets/text_area/#extending-textarea","title":"Extending TextArea","text":"

    Sometimes, you may wish to subclass TextArea to add some extra functionality. In this section, we'll briefly explore how we can extend the widget to achieve common goals.

    "},{"location":"widgets/text_area/#hooking-into-key-presses","title":"Hooking into key presses","text":"

    You may wish to hook into certain key presses to inject some functionality. This can be done by over-riding _on_key and adding the required functionality.

    "},{"location":"widgets/text_area/#example-closing-parentheses-automatically","title":"Example - closing parentheses automatically","text":"

    Let's extend TextArea to add a feature which automatically closes parentheses and moves the cursor to a sensible location.

    from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\n\nclass ExtendedTextArea(TextArea):\n    \"\"\"A subclass of TextArea with parenthesis-closing functionality.\"\"\"\n\n    def _on_key(self, event: events.Key) -> None:\n        if event.character == \"(\":\n            self.insert(\"()\")\n            self.move_cursor_relative(columns=-1)\n            event.prevent_default()\n\n\nclass TextAreaKeyPressHook(App):\n    def compose(self) -> ComposeResult:\n        yield ExtendedTextArea(language=\"python\")\n\n\napp = TextAreaKeyPressHook()\nif __name__ == \"__main__\":\n    app.run()\n

    This intercepts the key handler when \"(\" is pressed, and inserts \"()\" instead. It then moves the cursor so that it lands between the open and closing parentheses.

    Typing \"def hello(\" into the TextArea now results in the bracket automatically being closed:

    TextAreaKeyPressHook \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adef\u00a0hello()\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    "},{"location":"widgets/text_area/#advanced-concepts","title":"Advanced concepts","text":""},{"location":"widgets/text_area/#syntax-highlighting","title":"Syntax highlighting","text":"

    Syntax highlighting inside the TextArea is powered by a library called tree-sitter.

    Each time you update the document in a TextArea, an internal syntax tree is updated. This tree is frequently queried to find location ranges relevant to syntax highlighting. We give these ranges names, and ultimately map them to Rich styles inside TextAreaTheme.syntax_styles.

    To illustrate how this works, lets look at how the \"Monokai\" TextAreaTheme highlights Markdown files.

    When the language attribute is set to \"markdown\", a highlight query similar to the one below is used (trimmed for brevity).

    (heading_content) @heading\n(link) @link\n

    This highlight query maps heading_content nodes returned by the Markdown parser to the name @heading, and link nodes to the name @link.

    Inside our TextAreaTheme.syntax_styles dict, we can map the name @heading to a Rich style. Here's a snippet from the \"Monokai\" theme which does just that:

    TextAreaTheme(\n    name=\"monokai\",\n    base_style=Style(color=\"#f8f8f2\", bgcolor=\"#272822\"),\n    gutter_style=Style(color=\"#90908a\", bgcolor=\"#272822\"),\n    # ...\n    syntax_styles={\n        # Colorise @heading and make them bold\n        \"heading\": Style(color=\"#F92672\", bold=True),\n        # Colorise and underline @link\n        \"link\": Style(color=\"#66D9EF\", underline=True),\n        # ...\n    },\n)\n

    To understand which names can be mapped inside syntax_styles, we recommend looking at the existing themes and highlighting queries (.scm files) in the Textual repository.

    Tip

    You may also wish to take a look at the contents of TextArea._highlights on an active TextArea instance to see which highlights have been generated for the open document.

    "},{"location":"widgets/text_area/#adding-support-for-custom-languages","title":"Adding support for custom languages","text":"

    To add support for a language to a TextArea, use the register_language method.

    To register a language, we require two things:

    1. A tree-sitter Language object which contains the grammar for the language.
    2. A highlight query which is used for syntax highlighting.
    "},{"location":"widgets/text_area/#example-adding-java-support","title":"Example - adding Java support","text":"

    The easiest way to obtain a Language object is using the py-tree-sitter-languages package. Here's how we can use this package to obtain a reference to a Language object representing Java:

    from tree_sitter_languages import get_language\njava_language = get_language(\"java\")\n

    The exact version of the parser used when you call get_language can be checked via the repos.txt file in the version of py-tree-sitter-languages you're using. This file contains links to the GitHub repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at queries/highlights.scm, and a file showing all the available node types which can be used in highlight queries at src/node-types.json.

    Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps:

    1. Open repos.txt file from the py-tree-sitter-languages repo.
    2. Find the link corresponding to tree-sitter-java and go to the repo on GitHub (you may also need to go to the specific commit referenced in repos.txt).
    3. Go to queries/highlights.scm to see the example highlight query for Java.

    Be sure to check the license in the repo to ensure it can be freely copied.

    Warning

    It's important to use a highlight query which is compatible with the parser in use, so pay attention to the commit hash when visiting the repo via repos.txt.

    We now have our Language and our highlight query, so we can register Java as a language.

    from pathlib import Path\n\nfrom tree_sitter_languages import get_language\n\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\njava_language = get_language(\"java\")\njava_highlight_query = (Path(__file__).parent / \"java_highlights.scm\").read_text()\njava_code = \"\"\"\\\nclass HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, World!\");\n    }\n}\n\"\"\"\n\n\nclass TextAreaCustomLanguage(App):\n    def compose(self) -> ComposeResult:\n        text_area = TextArea(text=java_code)\n        text_area.cursor_blink = False\n\n        # Register the Java language and highlight query\n        text_area.register_language(java_language, java_highlight_query)\n\n        # Switch to Java\n        text_area.language = \"java\"\n        yield text_area\n\n\napp = TextAreaCustomLanguage()\nif __name__ == \"__main__\":\n    app.run()\n

    Running our app, we can see that the Java code is highlighted. We can freely edit the text, and the syntax highlighting will update immediately.

    TextAreaCustomLanguage \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aclass\u00a0HelloWorld\u00a0{\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0public\u00a0static\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0\u258e \u258a\u00a0\u00a0\u00a0\u00a0}\u00a0\u258e \u258a}\u00a0\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

    Recall that we map names (like @heading) from the tree-sitter highlight query to Rich style objects inside the TextAreaTheme.syntax_styles dictionary. If you notice some highlights are missing after registering a language, the issue may be:

    1. The current TextAreaTheme doesn't contain a mapping for the name in the highlight query. Adding a new to syntax_styles should resolve the issue.
    2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name.

    Tip

    The names assigned in tree-sitter highlight queries are often reused across multiple languages. For example, @string is used in many languages to highlight strings.

    "},{"location":"widgets/text_area/#navigation-and-wrapping-information","title":"Navigation and wrapping information","text":"

    If you're building functionality on top of TextArea, it may be useful to inspect the navigator and wrapped_document attributes.

    • navigator is a DocumentNavigator instance which can give us general information about the cursor's location within a document, as well as where the cursor will move to when certain actions are performed.
    • wrapped_document is a WrappedDocument instance which can be used to convert document locations to visual locations, taking wrapping into account. It also offers a variety of other convenience methods and properties.

    A detailed view of these classes is out of scope, but do note that a lot of the functionality of TextArea exists within them, so inspecting them could be worthwhile.

    "},{"location":"widgets/text_area/#reactive-attributes","title":"Reactive attributes","text":"Name Type Default Description language str | None None The language to use for syntax highlighting. theme str | None TextAreaTheme.default() The theme to use for syntax highlighting. selection Selection Selection() The current selection. show_line_numbers bool False Show or hide line numbers. indent_width int 4 The number of spaces to indent and width of tabs. match_cursor_bracket bool True Enable/disable highlighting matching brackets under cursor. cursor_blink bool True Enable/disable blinking of the cursor when the widget has focus. soft_wrap bool True Enable/disable soft wrapping."},{"location":"widgets/text_area/#messages","title":"Messages","text":"
    • TextArea.Changed
    • TextArea.SelectionChanged
    "},{"location":"widgets/text_area/#bindings","title":"Bindings","text":"

    The TextArea widget defines the following bindings:

    Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#component-classes","title":"Component classes","text":"

    The TextArea defines component classes that can style various aspects of the widget. Styles from the theme attribute take priority.

    TextArea offers some component classes which can be used to style aspects of the widget.

    Note that any attributes provided in the chosen TextAreaTheme will take priority here.

    Class Description text-area--cursor Target the cursor. text-area--gutter Target the gutter (line number column). text-area--cursor-gutter Target the gutter area of the line the cursor is on. text-area--cursor-line Target the line the cursor is on. text-area--selection Target the current selection. text-area--matching-bracket Target matching brackets."},{"location":"widgets/text_area/#see-also","title":"See also","text":"
    • Input - single-line text input widget
    • TextAreaTheme - theming the TextArea
    • DocumentNavigator - guides cursor movement
    • WrappedDocument - manages wrapping the document
    • The tree-sitter documentation website.
    • The tree-sitter Python bindings repository.
    • py-tree-sitter-languages repository (provides binary wheels for a large variety of tree-sitter languages).
    "},{"location":"widgets/text_area/#additional-notes","title":"Additional notes","text":"
    • To remove the outline effect when the TextArea is focused, you can set border: none; padding: 0; in your CSS.
    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea","title":"textual.widgets._text_area.TextArea class","text":"
    def __init__(\n    self,\n    text=\"\",\n    *,\n    language=None,\n    theme=None,\n    soft_wrap=True,\n    tab_behaviour=\"focus\",\n    show_line_numbers=False,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: ScrollView

    Parameters Parameter Default Description text str ''

    The initial text to load into the TextArea.

    language str | None None

    The language to use.

    theme str | None None

    The theme to use.

    soft_wrap bool True

    Enable soft wrapping.

    tab_behaviour Literal['focus', 'indent'] 'focus'

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    show_line_numbers bool False

    Show line numbers on the left edge.

    name str | None None

    The name of the TextArea widget.

    id str | None None

    The ID of the widget, used to refer to it from Textual CSS.

    classes str | None None

    One or more Textual CSS compatible class names separated by spaces.

    disabled bool False

    True if the widget is disabled.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"escape\",\n        \"screen.focus_next\",\n        \"Shift Focus\",\n        show=False,\n    ),\n    Binding(\"up\", \"cursor_up\", \"cursor up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"cursor down\", show=False\n    ),\n    Binding(\n        \"left\", \"cursor_left\", \"cursor left\", show=False\n    ),\n    Binding(\n        \"right\", \"cursor_right\", \"cursor right\", show=False\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_word_left\",\n        \"cursor word left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_word_right\",\n        \"cursor word right\",\n        show=False,\n    ),\n    Binding(\n        \"home,ctrl+a\",\n        \"cursor_line_start\",\n        \"cursor line start\",\n        show=False,\n    ),\n    Binding(\n        \"end,ctrl+e\",\n        \"cursor_line_end\",\n        \"cursor line end\",\n        show=False,\n    ),\n    Binding(\n        \"pageup\",\n        \"cursor_page_up\",\n        \"cursor page up\",\n        show=False,\n    ),\n    Binding(\n        \"pagedown\",\n        \"cursor_page_down\",\n        \"cursor page down\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+shift+left\",\n        \"cursor_word_left(True)\",\n        \"cursor left word select\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+shift+right\",\n        \"cursor_word_right(True)\",\n        \"cursor right word select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+home\",\n        \"cursor_line_start(True)\",\n        \"cursor line start select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+end\",\n        \"cursor_line_end(True)\",\n        \"cursor line end select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+up\",\n        \"cursor_up(True)\",\n        \"cursor up select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+down\",\n        \"cursor_down(True)\",\n        \"cursor down select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+left\",\n        \"cursor_left(True)\",\n        \"cursor left select\",\n        show=False,\n    ),\n    Binding(\n        \"shift+right\",\n        \"cursor_right(True)\",\n        \"cursor right select\",\n        show=False,\n    ),\n    Binding(\"f6\", \"select_line\", \"select line\", show=False),\n    Binding(\"f7\", \"select_all\", \"select all\", show=False),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"delete left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+w\",\n        \"delete_word_left\",\n        \"delete left to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"delete right\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+f\",\n        \"delete_word_right\",\n        \"delete right to start of word\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+x\", \"delete_line\", \"delete line\", show=False\n    ),\n    Binding(\n        \"ctrl+u\",\n        \"delete_to_start_of_line\",\n        \"delete to line start\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+k\",\n        \"delete_to_end_of_line\",\n        \"delete to line end\",\n        show=False,\n    ),\n]\n
    Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"text-area--cursor\",\n    \"text-area--gutter\",\n    \"text-area--cursor-gutter\",\n    \"text-area--cursor-line\",\n    \"text-area--selection\",\n    \"text-area--matching-bracket\",\n}\n

    TextArea offers some component classes which can be used to style aspects of the widget.

    Note that any attributes provided in the chosen TextAreaTheme will take priority here.

    Class Description text-area--cursor Target the cursor. text-area--gutter Target the gutter (line number column). text-area--cursor-gutter Target the gutter area of the line the cursor is on. text-area--cursor-line Target the line the cursor is on. text-area--selection Target the current selection. text-area--matching-bracket Target matching brackets."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_languages","title":"available_languages property","text":"
    available_languages: set[str]\n

    A list of the names of languages available to the TextArea.

    The values in this list can be assigned to the language reactive attribute of TextArea.

    The returned list contains the builtin languages plus those registered via the register_language method. Builtin languages will be listed before user-registered languages, but there are no other ordering guarantees.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_themes","title":"available_themes property","text":"
    available_themes: set[str]\n

    A list of the names of the themes available to the TextArea.

    The values in this list can be assigned theme reactive attribute of TextArea.

    You can retrieve the full specification for a theme by passing one of the strings from this list into TextAreaTheme.get_by_name(theme_name: str).

    Alternatively, you can directly retrieve a list of TextAreaTheme objects (which contain the full theme specification) by calling TextAreaTheme.builtin_themes().

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_line","title":"cursor_at_end_of_line property","text":"
    cursor_at_end_of_line: bool\n

    True if and only if the cursor is at the end of a row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_text","title":"cursor_at_end_of_text property","text":"
    cursor_at_end_of_text: bool\n

    True if and only if the cursor is at the very end of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_first_line","title":"cursor_at_first_line property","text":"
    cursor_at_first_line: bool\n

    True if and only if the cursor is on the first line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_last_line","title":"cursor_at_last_line property","text":"
    cursor_at_last_line: bool\n

    True if and only if the cursor is on the last line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_line","title":"cursor_at_start_of_line property","text":"
    cursor_at_start_of_line: bool\n

    True if and only if the cursor is at column 0.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_text","title":"cursor_at_start_of_text property","text":"
    cursor_at_start_of_text: bool\n

    True if and only if the cursor is at location (0, 0)

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_blink","title":"cursor_blink class-attribute instance-attribute","text":"
    cursor_blink: Reactive[bool] = reactive(True, init=False)\n

    True if the cursor should blink.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_location","title":"cursor_location writable property","text":"
    cursor_location: Location\n

    The current location of the cursor in the document.

    This is a utility for accessing the end of TextArea.selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset: Offset\n

    The offset of the cursor relative to the screen.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.document","title":"document instance-attribute","text":"
    document: DocumentBase = Document(text)\n

    The document this widget is currently editing.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.gutter_width","title":"gutter_width property","text":"
    gutter_width: int\n

    The width of the gutter (the left column containing line numbers).

    Returns Type Description int

    The cell-width of the line number column. If show_line_numbers is False returns 0.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
    indent_type: Literal['tabs', 'spaces'] = 'spaces'\n

    Whether to indent using tabs or spaces.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_width","title":"indent_width class-attribute instance-attribute","text":"
    indent_width: Reactive[int] = reactive(4, init=False)\n

    The width of tabs or the multiple of spaces to align to on pressing the tab key.

    If the document currently open contains tabs that are currently visible on screen, altering this value will immediately change the display width of the visible tabs.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.is_syntax_aware","title":"is_syntax_aware property","text":"
    is_syntax_aware: bool\n

    True if the TextArea is currently syntax aware - i.e. it's parsing document content.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.language","title":"language class-attribute instance-attribute","text":"
    language: Reactive[str | None] = language\n

    The language to use.

    This must be set to a valid, non-None value for syntax highlighting to work.

    If the value is a string, a built-in language parser will be used if available.

    If you wish to use an unsupported language, you'll have to register it first using TextArea.register_language.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.match_cursor_bracket","title":"match_cursor_bracket class-attribute instance-attribute","text":"
    match_cursor_bracket: Reactive[bool] = reactive(\n    True, init=False\n)\n

    If the cursor is at a bracket, highlight the matching bracket (if found).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.navigator","title":"navigator instance-attribute","text":"
    navigator: DocumentNavigator = DocumentNavigator(\n    self.wrapped_document\n)\n

    Queried to determine where the cursor should move given a navigation action, accounting for wrapping etc.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
    selected_text: str\n

    The text between the start and end points of the current selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selection","title":"selection class-attribute instance-attribute","text":"
    selection: Reactive[Selection] = reactive(\n    Selection(), init=False, always_update=True\n)\n

    The selection start and end locations (zero-based line_index, offset).

    This represents the cursor location and the current selection.

    The Selection.end always refers to the cursor location.

    If no text is selected, then Selection.end == Selection.start is True.

    The text selected in the document is available via the TextArea.selected_text property.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.show_line_numbers","title":"show_line_numbers class-attribute instance-attribute","text":"
    show_line_numbers: Reactive[bool] = reactive(\n    False, init=False\n)\n

    True to show the line number column on the left edge, otherwise False.

    Changing this value will immediately re-render the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.soft_wrap","title":"soft_wrap class-attribute instance-attribute","text":"
    soft_wrap: Reactive[bool] = reactive(True, init=False)\n

    True if text should soft wrap.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text writable property","text":"
    text: str\n

    The entire text content of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.theme","title":"theme class-attribute instance-attribute","text":"
    theme: Reactive[str | None] = theme\n

    The name of the theme to use.

    Themes must be registered using TextArea.register_theme before they can be used.

    Syntax highlighting is only possible when the language attribute is set.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.wrap_width","title":"wrap_width property","text":"
    wrap_width: int\n

    The width which gets used when the document wraps.

    Accounts for gutter, scrollbars, etc.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.wrapped_document","title":"wrapped_document instance-attribute","text":"
    wrapped_document: WrappedDocument = WrappedDocument(\n    self.document\n)\n

    The wrapped view of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed","title":"Changed class","text":"

    Bases: Message

    Posted when the content inside the TextArea changes.

    Handle this message using the on decorator - @on(TextArea.Changed) or a method named on_text_area_changed.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed.control","title":"control property","text":"
    control: TextArea\n

    The TextArea that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed.text_area","title":"text_area instance-attribute","text":"
    text_area: TextArea\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged","title":"SelectionChanged class","text":"

    Bases: Message

    Posted when the selection changes.

    This includes when the cursor moves or when text is selected.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.selection","title":"selection instance-attribute","text":"
    selection: Selection\n

    The new selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.text_area","title":"text_area instance-attribute","text":"
    text_area: TextArea\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self, select=False):\n

    Move the cursor down one cell.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left method","text":"
    def action_cursor_left(self, select=False):\n

    Move the cursor one location to the left.

    If the cursor is at the left edge of the document, try to move it to the end of the previous line.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end method","text":"
    def action_cursor_line_end(self, select=False):\n

    Move the cursor to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_start","title":"action_cursor_line_start method","text":"
    def action_cursor_line_start(self, select=False):\n

    Move the cursor to the start of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_down","title":"action_cursor_page_down method","text":"
    def action_cursor_page_down(self):\n

    Move the cursor and scroll down one page.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_up","title":"action_cursor_page_up method","text":"
    def action_cursor_page_up(self):\n

    Move the cursor and scroll up one page.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right","title":"action_cursor_right method","text":"
    def action_cursor_right(self, select=False):\n

    Move the cursor one location to the right.

    If the cursor is at the end of a line, attempt to go to the start of the next line.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self, select=False):\n

    Move the cursor up one cell.

    Parameters Parameter Default Description select bool False

    If True, select the text while moving.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left method","text":"
    def action_cursor_word_left(self, select=False):\n

    Move the cursor left by a single word, skipping trailing whitespace.

    Parameters Parameter Default Description select bool False

    Whether to select while moving the cursor.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right method","text":"
    def action_cursor_word_right(self, select=False):\n

    Move the cursor right by a single word, skipping leading whitespace.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_left","title":"action_delete_left method","text":"
    def action_delete_left(self):\n

    Deletes the character to the left of the cursor and updates the cursor location.

    If there's a selection, then the selected range is deleted.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_line","title":"action_delete_line method","text":"
    def action_delete_line(self):\n

    Deletes the lines which intersect with the selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_right","title":"action_delete_right method","text":"
    def action_delete_right(self):\n

    Deletes the character to the right of the cursor and keeps the cursor at the same location.

    If there's a selection, then the selected range is deleted.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line","title":"action_delete_to_end_of_line method","text":"
    def action_delete_to_end_of_line(self):\n

    Deletes from the cursor location to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line method","text":"
    def action_delete_to_start_of_line(self):\n

    Deletes from the cursor location to the start of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_left","title":"action_delete_word_left method","text":"
    def action_delete_word_left(self):\n

    Deletes the word to the left of the cursor and updates the cursor location.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_right","title":"action_delete_word_right method","text":"
    def action_delete_word_right(self):\n

    Deletes the word to the right of the cursor and keeps the cursor at the same location.

    Note that the location that we delete to using this action is not the same as the location we move to when we move the cursor one word to the right. This action does not skip leading whitespace, whereas cursor movement does.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all method","text":"
    def action_select_all(self):\n

    Select all the text in the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line method","text":"
    def action_select_line(self):\n

    Select all the text on the current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index method","text":"
    def cell_width_to_column_index(self, cell_width, row_index):\n

    Return the column that the cell width corresponds to on the given row.

    Parameters Parameter Default Description cell_width int required

    The cell width to convert.

    row_index int required

    The index of the row to examine.

    Returns Type Description int

    The column corresponding to the cell width on that row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable method","text":"
    def clamp_visitable(self, location):\n

    Clamp the given location to the nearest visitable location.

    Parameters Parameter Default Description location Location required

    The location to clamp.

    Returns Type Description Location

    The nearest location that we could conceivably navigate to using the cursor.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear method","text":"
    def clear(self):\n

    Delete all text from the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor","title":"code_editor classmethod","text":"
    def code_editor(\n    cls,\n    text=\"\",\n    *,\n    language=None,\n    theme=\"monokai\",\n    soft_wrap=False,\n    tab_behaviour=\"indent\",\n    show_line_numbers=True,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Construct a new TextArea with sensible defaults for editing code.

    This instantiates a TextArea with line numbers enabled, soft wrapping disabled, and \"indent\" tab behaviour.

    Parameters Parameter Default Description text str ''

    The initial text to load into the TextArea.

    language str | None None

    The language to use.

    theme str | None 'monokai'

    The theme to use.

    soft_wrap bool False

    Enable soft wrapping.

    tab_behaviour Literal['focus', 'indent'] 'indent'

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    show_line_numbers bool True

    Show line numbers on the left edge.

    name str | None None

    The name of the TextArea widget.

    id str | None None

    The ID of the widget, used to refer to it from Textual CSS.

    classes str | None None

    One or more Textual CSS compatible class names separated by spaces.

    disabled bool False

    True if the widget is disabled.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete method","text":"
    def delete(self, start, end, *, maintain_selection_offset=True):\n

    Delete the text between two locations in the document.

    Parameters Parameter Default Description start Location required

    The start location.

    end Location required

    The end location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit method","text":"
    def edit(self, edit):\n

    Perform an Edit.

    Parameters Parameter Default Description edit Edit required

    The Edit to perform.

    Returns Type Description EditResult

    Data relating to the edit that may be useful. The data returned

    EditResult

    may be different depending on the edit performed.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket method","text":"
    def find_matching_bracket(self, bracket, search_from):\n

    If the character is a bracket, find the matching bracket.

    Parameters Parameter Default Description bracket str required

    The character we're searching for the matching bracket of.

    search_from Location required

    The location to start the search.

    Returns Type Description Location | None

    The Location of the matching bracket, or None if it's not found.

    Location | None

    If the character is not available for bracket matching, None is returned.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width method","text":"
    def get_column_width(self, row, column):\n

    Get the cell offset of the column from the start of the row.

    Parameters Parameter Default Description row int required

    The row index.

    column int required

    The column index (codepoint offset from start of row).

    Returns Type Description int

    The cell width of the column relative to the start of the row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location method","text":"
    def get_cursor_down_location(self):\n

    Get the location the cursor will move to if it moves down.

    Returns Type Description Location

    The location the cursor will move to if it moves down.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_left_location","title":"get_cursor_left_location method","text":"
    def get_cursor_left_location(self):\n

    Get the location the cursor will move to if it moves left.

    Returns Type Description Location

    The location of the cursor if it moves left.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_end_location","title":"get_cursor_line_end_location method","text":"
    def get_cursor_line_end_location(self):\n

    Get the location of the end of the current line.

    Returns Type Description Location

    The (row, column) location of the end of the cursors current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_start_location","title":"get_cursor_line_start_location method","text":"
    def get_cursor_line_start_location(self, smart_home=False):\n

    Get the location of the start of the current line.

    Parameters Parameter Default Description smart_home bool False

    If True, use \"smart home key\" behavior - go to the first non-whitespace character on the line, and if already there, go to offset 0. Smart home only works when wrapping is disabled.

    Returns Type Description Location

    The (row, column) location of the start of the cursors current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location method","text":"
    def get_cursor_right_location(self):\n

    Get the location the cursor will move to if it moves right.

    Returns Type Description Location

    the location the cursor will move to if it moves right.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_up_location","title":"get_cursor_up_location method","text":"
    def get_cursor_up_location(self):\n

    Get the location the cursor will move to if it moves up.

    Returns Type Description Location

    The location the cursor will move to if it moves up.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_left_location","title":"get_cursor_word_left_location method","text":"
    def get_cursor_word_left_location(self):\n

    Get the location the cursor will jump to if it goes 1 word left.

    Returns Type Description Location

    The location the cursor will jump on \"jump word left\".

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_right_location","title":"get_cursor_word_right_location method","text":"
    def get_cursor_word_right_location(self):\n

    Get the location the cursor will jump to if it goes 1 word right.

    Returns Type Description Location

    The location the cursor will jump on \"jump word right\".

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location method","text":"
    def get_target_document_location(self, event):\n

    Given a MouseEvent, return the row and column offset of the event in document-space.

    Parameters Parameter Default Description event MouseEvent required

    The MouseEvent.

    Returns Type Description Location

    The location of the mouse event within the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range method","text":"
    def get_text_range(self, start, end):\n

    Get the text between a start and end location.

    Parameters Parameter Default Description start Location required

    The start location.

    end Location required

    The end location.

    Returns Type Description str

    The text between start and end.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert method","text":"
    def insert(\n    self,\n    text,\n    location=None,\n    *,\n    maintain_selection_offset=True\n):\n

    Insert text into the document.

    Parameters Parameter Default Description text str required

    The text to insert.

    location Location | None None

    The location to insert text, or None to use the cursor location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text method","text":"
    def load_text(self, text):\n

    Load text into the TextArea.

    This will replace the text currently in the TextArea.

    Parameters Parameter Default Description text str required

    The text to load into the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor method","text":"
    def move_cursor(\n    self,\n    location,\n    select=False,\n    center=False,\n    record_width=True,\n):\n

    Move the cursor to a location.

    Parameters Parameter Default Description location Location required

    The location to move the cursor to.

    select bool False

    If True, select text between the old and new location.

    center bool False

    If True, scroll such that the cursor is centered.

    record_width bool True

    If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative method","text":"
    def move_cursor_relative(\n    self,\n    rows=0,\n    columns=0,\n    select=False,\n    center=False,\n    record_width=True,\n):\n

    Move the cursor relative to its current location in document-space.

    Parameters Parameter Default Description rows int 0

    The number of rows to move down by (negative to move up)

    columns int 0

    The number of columns to move right by (negative to move left)

    select bool False

    If True, select text between the old and new location.

    center bool False

    If True, scroll such that the cursor is centered.

    record_width bool True

    If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width method","text":"
    def record_cursor_width(self):\n

    Record the current cell width of the cursor.

    This is used where we navigate up and down through rows. If we're in the middle of a row, and go down to a row with no content, then we go down to another row, we want our cursor to jump back to the same offset that we were originally at.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language method","text":"
    def register_language(self, language, highlight_query):\n

    Register a language and corresponding highlight query.

    Calling this method does not change the language of the TextArea. On switching to this language (via the language reactive attribute), syntax highlighting will be performed using the given highlight query.

    If a string name is supplied for a builtin supported language, then this method will update the default highlight query for that language.

    Registering a language only registers it to this instance of TextArea.

    Parameters Parameter Default Description language str | 'Language' required

    A string referring to a builtin language or a tree-sitter Language object.

    highlight_query str required

    The highlight query to use for syntax highlighting this language.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme method","text":"
    def register_theme(self, theme):\n

    Register a theme for use by the TextArea.

    After registering a theme, you can set themes by assigning the theme name to the TextArea.theme reactive attribute. For example text_area.theme = \"my_custom_theme\" where \"my_custom_theme\" is the name of the theme you registered.

    If you supply a theme with a name that already exists that theme will be overwritten.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace","title":"replace method","text":"
    def replace(\n    self,\n    insert,\n    start,\n    end,\n    *,\n    maintain_selection_offset=True\n):\n

    Replace text in the document with new text.

    Parameters Parameter Default Description insert str required

    The text to insert.

    start Location required

    The start location

    end Location required

    The end location.

    maintain_selection_offset bool True

    If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

    Returns Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible method","text":"
    def scroll_cursor_visible(self, center=False, animate=False):\n

    Scroll the TextArea such that the cursor is visible on screen.

    Parameters Parameter Default Description center bool False

    True if the cursor should be scrolled to the center.

    animate bool False

    True if we should animate while scrolling.

    Returns Type Description Offset

    The offset that was scrolled to bring the cursor into view.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all method","text":"
    def select_all(self):\n

    Select all of the text in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line method","text":"
    def select_line(self, index):\n

    Select all the text in the specified line.

    Parameters Parameter Default Description index int required

    The index of the line to select (starting from 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Highlight","title":"Highlight module-attribute","text":"
    Highlight = Tuple[StartColumn, EndColumn, HighlightName]\n

    A tuple representing a syntax highlight within one line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Location","title":"Location module-attribute","text":"
    Location = Tuple[int, int]\n

    A location (row, column) within the document. Indexing starts at 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document","title":"Document class","text":"
    def __init__(self, text):\n

    Bases: DocumentBase

    A document which can be opened in a TextArea.

    "},{"location":"widgets/text_area/#textual.document._document.Document.line_count","title":"line_count property","text":"
    line_count: int\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.lines","title":"lines property","text":"
    lines: list[str]\n

    Get the document as a list of strings, where each string represents a line.

    Newline characters are not included in at the end of the strings.

    The newline character used in this document can be found via the Document.newline property.

    "},{"location":"widgets/text_area/#textual.document._document.Document.newline","title":"newline property","text":"
    newline: Newline\n

    Get the Newline used in this document (e.g. ' ', ' '. etc.)

    "},{"location":"widgets/text_area/#textual.document._document.Document.text","title":"text property","text":"
    text: str\n

    Get the text from the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_index_from_location","title":"get_index_from_location method","text":"
    def get_index_from_location(self, location):\n

    Given a location, returns the index from the document's text.

    Parameters Parameter Default Description location Location required

    The location in the document.

    Returns Type Description int

    The index in the document's text.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_line","title":"get_line method","text":"
    def get_line(self, index):\n

    Returns the line with the given index from the document.

    Parameters Parameter Default Description index int required

    The index of the line in the document.

    Returns Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_location_from_index","title":"get_location_from_index method","text":"
    def get_location_from_index(self, index):\n

    Given an index in the document's text, returns the corresponding location.

    Parameters Parameter Default Description index int required

    The index in the document's text.

    Returns Type Description Location

    The corresponding location.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_size","title":"get_size method","text":"
    def get_size(self, tab_width):\n

    The Size of the document, taking into account the tab rendering width.

    Parameters Parameter Default Description tab_width int required

    The width to use for tab indents.

    Returns Type Description Size

    The size (width, height) of the document.

    "},{"location":"widgets/text_area/#textual.document._document.Document.get_text_range","title":"get_text_range method","text":"
    def get_text_range(self, start, end):\n

    Get the text that falls between the start and end locations.

    Returns the text between start and end, including the appropriate line separator character as specified by Document._newline. Note that _newline is set automatically to the first line separator character found in the document.

    Parameters Parameter Default Description start Location required

    The start location of the selection.

    end Location required

    The end location of the selection.

    Returns Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.document._document.Document.replace_range","title":"replace_range method","text":"
    def replace_range(self, start, end, text):\n

    Replace text at the given range.

    This is the only method by which a document may be updated.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The EditResult containing information about the completed replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase class","text":"

    Bases: ABC

    Describes the minimum functionality a Document implementation must provide in order to be used by the TextArea widget.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.line_count","title":"line_count property abstractmethod","text":"
    line_count: int\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.lines","title":"lines property abstractmethod","text":"
    lines: list[str]\n

    Get the lines of the document as a list of strings.

    The strings should not include newline characters. The newline character used for the document can be retrieved via the newline property.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.newline","title":"newline property abstractmethod","text":"
    newline: Newline\n

    Return the line separator used in the document.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.text","title":"text property abstractmethod","text":"
    text: str\n

    The text from the document as a string.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_line","title":"get_line abstractmethod","text":"
    def get_line(self, index):\n

    Returns the line with the given index from the document.

    This is used in rendering lines, and will be called by the TextArea for each line that is rendered.

    Parameters Parameter Default Description index int required

    The index of the line in the document.

    Returns Type Description str

    The str instance representing the line.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_size","title":"get_size abstractmethod","text":"
    def get_size(self, indent_width):\n

    Get the size of the document.

    The height is generally the number of lines, and the width is generally the maximum cell length of all the lines.

    Parameters Parameter Default Description indent_width int required

    The width to use for tab characters.

    Returns Type Description Size

    The Size of the document bounding box.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
    def get_text_range(self, start, end):\n

    Get the text that falls between the start and end locations.

    Parameters Parameter Default Description start Location required

    The start location of the selection.

    end Location required

    The end location of the selection.

    Returns Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.query_syntax_tree","title":"query_syntax_tree method","text":"
    def query_syntax_tree(\n    self, query, start_point=None, end_point=None\n):\n

    Query the tree-sitter syntax tree.

    The default implementation always returns an empty list.

    To support querying in a subclass, this must be implemented.

    Parameters Parameter Default Description query Query required

    The tree-sitter Query to perform.

    start_point tuple[int, int] | None None

    The (row, column byte) to start the query at.

    end_point tuple[int, int] | None None

    The (row, column byte) to end the query at.

    Returns Type Description list[tuple[Node, str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.document._document.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
    def replace_range(self, start, end, text):\n

    Replace the text at the given range.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator","title":"DocumentNavigator class","text":"
    def __init__(self, wrapped_document):\n

    Cursor navigation in the TextArea is \"wrapping-aware\".

    Although the cursor location (the selection) is represented as a location in the raw document, when you actually move the cursor, it must take wrapping into account (otherwise things start to look really confusing to the user where wrapping is involved).

    Your cursor visually moves through the wrapped version of the document, rather than the raw document. So, for example, pressing down on the keyboard may move your cursor to a position further along the current raw document line, rather than on to the next line in the raw document.

    The DocumentNavigator class manages that behaviour.

    Given a cursor location in the unwrapped document, and a cursor movement action, this class can inform us of the destination the cursor will move to considering the current wrapping width and document content. It can also translate between document-space (a location/(row,col) in the raw document), and visual-space (x and y offsets) as the user will see them on screen after the document has been wrapped.

    For this to work correctly, the wrapped_document and document must be synchronised. This means that if you make an edit to the document, you must then update the wrapped document, and then you may query the document navigator.

    Naming conventions:

    A \"location\" refers to a location, in document-space (in the raw document). It is entirely unrelated to visually positioning. A location in a document can appear in any visual position, as it is influenced by scrolling, wrapping, gutter settings, and the cell width of characters to its left.

    A \"wrapped section\" refers to a portion of the line accounting for wrapping. For example the line \"ABCDEF\" when wrapped at width 3 will result in 2 sections: \"ABC\" and \"DEF\". In this case, we call \"ABC\" is the first section/wrapped section.

    A \"wrap offset\" is an integer representing the index at which wrapping occurs in a document-space line. This is a codepoint index, rather than a visual offset. In \"ABCDEF\" with wrapping at width 3, there is a single wrap offset of 3.

    \"Smart home\" refers to a modification of the \"home\" key behaviour. If smart home is enabled, the first non-whitespace character is considered to be the home location. If the cursor is currently at this position, then the normal home behaviour applies. This is designed to make cursor movement more useful to end users.

    Parameters Parameter Default Description wrapped_document WrappedDocument required

    The WrappedDocument to be used when making navigation decisions.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.last_x_offset","title":"last_x_offset instance-attribute","text":"
    last_x_offset = 0\n

    Remembers the last x offset (cell width) the cursor was moved horizontally to, so that it can be restored on vertical movement where possible.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.clamp_reachable","title":"clamp_reachable method","text":"
    def clamp_reachable(self, location):\n

    Given a location, return the nearest location that corresponds to a reachable location in the document.

    Parameters Parameter Default Description location Location required

    A location.

    Returns Type Description Location

    The nearest reachable location in the document.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_above","title":"get_location_above method","text":"
    def get_location_above(self, location):\n

    Get the location visually aligned with the cell above the given location.

    Parameters Parameter Default Description location Location required

    The location to start from.

    Returns Type Description Location

    The cell above the given location.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_at_y_offset","title":"get_location_at_y_offset method","text":"
    def get_location_at_y_offset(self, location, vertical_offset):\n

    Apply a visual vertical offset to a location and check the resulting location.

    Parameters Parameter Default Description location Location required

    The location to start from.

    vertical_offset int required

    The vertical offset to move (negative=up, positive=down).

    Returns Type Description Location

    The location after the offset has been applied.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_below","title":"get_location_below method","text":"
    def get_location_below(self, location):\n

    Given a location in the raw document, return the raw document location corresponding to moving down in the wrapped representation of the document.

    Parameters Parameter Default Description location Location required

    The location in the raw document.

    Returns Type Description Location

    The location which is visually below the given location.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_end","title":"get_location_end method","text":"
    def get_location_end(self, location):\n

    Get the location corresponding to the end of the current section.

    Parameters Parameter Default Description location Location required

    The current location.

    Returns Type Description Location

    The location corresponding to the end of the wrapped line.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_home","title":"get_location_home method","text":"
    def get_location_home(self, location, smart_home=False):\n

    Get the \"home location\" corresponding to the given location.

    Parameters Parameter Default Description location Location required

    The location to consider.

    smart_home bool False

    Enable/disable 'smart home' behaviour.

    Returns Type Description Location

    The home location, relative to the given location.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_left","title":"get_location_left method","text":"
    def get_location_left(self, location):\n

    Get the location to the left of the given location.

    Note that if the given location is at the start of the line, then this will return the end of the preceding line, since that's where you would expect the cursor to move.

    Parameters Parameter Default Description location Location required

    The location to start from.

    Returns Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.get_location_right","title":"get_location_right method","text":"
    def get_location_right(self, location):\n

    Get the location to the right of the given location.

    Note that if the given location is at the end of the line, then this will return the start of the following line, since that's where you would expect the cursor to move.

    Parameters Parameter Default Description location Location required

    The location to start from.

    Returns Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_end_of_document","title":"is_end_of_document method","text":"
    def is_end_of_document(self, location):\n

    Check if a location is at the end of the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is at the end of the document.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_end_of_document_line","title":"is_end_of_document_line method","text":"
    def is_end_of_document_line(self, location):\n

    True if the location is at the end of a line in the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the document is on the last line of the document.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_end_of_wrapped_line","title":"is_end_of_wrapped_line method","text":"
    def is_end_of_wrapped_line(self, location):\n

    True if the location is at the end of a wrapped line.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is on the last wrapped section of any line.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_first_document_line","title":"is_first_document_line method","text":"
    def is_first_document_line(self, location):\n

    Check if the given location is on the first line in the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is on the first line of the document.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_first_wrapped_line","title":"is_first_wrapped_line method","text":"
    def is_first_wrapped_line(self, location):\n

    Check if the given location is on the first wrapped section of the first line in the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is on the first wrapped section of the first line.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_last_document_line","title":"is_last_document_line method","text":"
    def is_last_document_line(self, location):\n

    Check if the given location is on the last line of the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True when the location is on the last line of the document.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_last_wrapped_line","title":"is_last_wrapped_line method","text":"
    def is_last_wrapped_line(self, location):\n

    Check if the given location is on the last wrapped section of the last line.

    That is, the cursor is visually on the last rendered row.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is on the last section of the last line.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_start_of_document","title":"is_start_of_document method","text":"
    def is_start_of_document(self, location):\n

    Check if a location is at the start of the document.

    Parameters Parameter Default Description location Location required

    The location to examine.

    Returns Type Description bool

    True if and only if the cursor is at document location (0, 0)

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_start_of_document_line","title":"is_start_of_document_line method","text":"
    def is_start_of_document_line(self, location):\n

    True when the location is at the start of the first document line.

    Parameters Parameter Default Description location Location required

    The location to check.

    Returns Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.document._document_navigator.DocumentNavigator.is_start_of_wrapped_line","title":"is_start_of_wrapped_line method","text":"
    def is_start_of_wrapped_line(self, location):\n

    True when the location is at the start of the first wrapped line.

    Parameters Parameter Default Description location Location required

    The location to check.

    Returns Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit class","text":"

    Implements the Undoable protocol to replace text at some range within a document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.from_location","title":"from_location instance-attribute","text":"
    from_location: Location\n

    The start location of the insert.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.maintain_selection_offset","title":"maintain_selection_offset instance-attribute","text":"
    maintain_selection_offset: bool\n

    If True, the selection will maintain its offset to the replacement range.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.text","title":"text instance-attribute","text":"
    text: str\n

    The text to insert. An empty string is equivalent to deletion.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.to_location","title":"to_location instance-attribute","text":"
    to_location: Location\n

    The end location of the insert

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.after","title":"after method","text":"
    def after(self, text_area):\n

    Possibly update the cursor location after the widget has been refreshed.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea this operation was performed on.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.do","title":"do method","text":"
    def do(self, text_area):\n

    Perform the edit operation.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea to perform the edit on.

    Returns Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.undo","title":"undo method","text":"
    def undo(self, text_area):\n

    Undo the edit operation.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea to undo the insert operation on.

    Returns Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult class","text":"

    Contains information about an edit that has occurred.

    "},{"location":"widgets/text_area/#textual.document._document.EditResult.end_location","title":"end_location instance-attribute","text":"
    end_location: Location\n

    The new end Location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.document._document.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
    replaced_text: str\n

    The text that was replaced.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist class","text":"

    Bases: Exception

    Raised when the user tries to use a language which does not exist. This means a language which is not builtin, or has not been registered.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Selection","title":"Selection class","text":"

    Bases: NamedTuple

    A range of characters within a document from a start point to the end point. The location of the cursor is always considered to be the end point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.end","title":"end class-attribute instance-attribute","text":"
    end: Location = (0, 0)\n

    The end location of the selection.

    If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.is_empty","title":"is_empty property","text":"
    is_empty: bool\n

    Return True if the selection has 0 width, i.e. it's just a cursor.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.start","title":"start class-attribute instance-attribute","text":"
    start: Location = (0, 0)\n

    The start location of the selection.

    If you were to click and drag a selection inside a text-editor, this is where you started dragging.

    "},{"location":"widgets/text_area/#textual.document._document.Selection.cursor","title":"cursor classmethod","text":"
    def cursor(cls, location):\n

    Create a Selection with the same start and end point - a \"cursor\".

    Parameters Parameter Default Description location Location required

    The location to create the zero-width Selection.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument class","text":"
    def __init__(self, text, language):\n

    Bases: Document

    A wrapper around a Document which also maintains a tree-sitter syntax tree when the document is edited.

    The primary reason for this split is actually to keep tree-sitter stuff separate, since it isn't supported in Python 3.7. By having the tree-sitter code isolated in this subclass, it makes it easier to conditionally import. However, it does come with other design flaws (e.g. Document is required to have methods which only really make sense on SyntaxAwareDocument).

    If you're reading this and Python 3.7 is no longer supported by Textual, consider merging this subclass into the Document superclass.

    Parameters Parameter Default Description text str required

    The initial text contained in the document.

    language str | Language required

    The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
    language: Language | None = get_language(language)\n

    The tree-sitter Language or None if tree-sitter is unavailable.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.get_line","title":"get_line method","text":"
    def get_line(self, line_index):\n

    Return the string representing the line, not including new line characters.

    Parameters Parameter Default Description line_index int required

    The index of the line.

    Returns Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.prepare_query","title":"prepare_query method","text":"
    def prepare_query(self, query):\n

    Prepare a tree-sitter tree query.

    Queries should be prepared once, then reused.

    To execute a query, call query_syntax_tree.

    Parameters Parameter Default Description query str required

    The string query to prepare.

    Returns Type Description Query | None

    The prepared query.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree method","text":"
    def query_syntax_tree(\n    self, query, start_point=None, end_point=None\n):\n

    Query the tree-sitter syntax tree.

    The default implementation always returns an empty list.

    To support querying in a subclass, this must be implemented.

    Parameters Parameter Default Description query Query required

    The tree-sitter Query to perform.

    start_point tuple[int, int] | None None

    The (row, column byte) to start the query at.

    end_point tuple[int, int] | None None

    The (row, column byte) to end the query at.

    Returns Type Description list[tuple['Node', str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.replace_range","title":"replace_range method","text":"
    def replace_range(self, start, end, text):\n

    Replace text at the given range.

    Parameters Parameter Default Description start Location required

    A tuple (row, column) where the edit starts.

    end Location required

    A tuple (row, column) where the edit ends.

    text str required

    The text to insert between start and end.

    Returns Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme class","text":"

    A theme for the TextArea widget.

    Allows theming the general widget (gutter, selections, cursor, and so on) and mapping of tree-sitter tokens to Rich styles.

    For example, consider the following snippet from the markdown.scm highlight query file. We've assigned the heading_content token type to the name heading.

    (heading_content) @heading\n

    Now, we can map this heading name to a Rich style, and it will be styled as such in the TextArea, assuming a parser which returns a heading_content node is used (as will be the case when language=\"markdown\").

    TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})\n

    We can register this theme with our TextArea using the TextArea.register_theme method, and headings in our markdown files will be styled bold cyan.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.base_style","title":"base_style class-attribute instance-attribute","text":"
    base_style: Style | None = None\n

    The background style of the text area. If None the parent style will be used.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style class-attribute instance-attribute","text":"
    bracket_matching_style: Style | None = None\n

    The style to apply to matching brackets. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style class-attribute instance-attribute","text":"
    cursor_line_gutter_style: Style | None = None\n

    The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_style","title":"cursor_line_style class-attribute instance-attribute","text":"
    cursor_line_style: Style | None = None\n

    The style to apply to the line the cursor is on.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_style","title":"cursor_style class-attribute instance-attribute","text":"
    cursor_style: Style | None = None\n

    The style of the cursor. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.gutter_style","title":"gutter_style class-attribute instance-attribute","text":"
    gutter_style: Style | None = None\n

    The style of the gutter. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.name","title":"name instance-attribute","text":"
    name: str\n

    The name of the theme.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.selection_style","title":"selection_style class-attribute instance-attribute","text":"
    selection_style: Style | None = None\n

    The style of the selection. If None a default selection Style will be generated.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.syntax_styles","title":"syntax_styles class-attribute instance-attribute","text":"
    syntax_styles: dict[str, Style] = field(\n    default_factory=dict\n)\n

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.apply_css","title":"apply_css method","text":"
    def apply_css(self, text_area):\n

    Apply CSS rules from a TextArea to be used for fallback styling.

    If any attributes in the theme aren't supplied, they'll be filled with the appropriate base CSS (e.g. color, background, etc.) and component CSS (e.g. text-area--cursor) from the supplied TextArea.

    Parameters Parameter Default Description text_area TextArea required

    The TextArea instance to retrieve fallback styling from.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
    def builtin_themes(cls):\n

    Get a list of all builtin TextAreaThemes.

    Returns Type Description list[TextAreaTheme]

    A list of all builtin TextAreaThemes.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.default","title":"default classmethod","text":"
    def default(cls):\n

    Get the default syntax theme.

    Returns Type Description TextAreaTheme

    The default TextAreaTheme (probably \"css\").

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
    def get_builtin_theme(cls, theme_name):\n

    Get a TextAreaTheme by name.

    Given a theme_name, return the corresponding TextAreaTheme object.

    Parameters Parameter Default Description theme_name str required

    The name of the theme.

    Returns Type Description TextAreaTheme | None

    The TextAreaTheme corresponding to the name or None if the theme isn't found.

    "},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_highlight","title":"get_highlight method","text":"
    def get_highlight(self, name):\n

    Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

    Parameters Parameter Default Description name str required

    The name of the highlight.

    Returns Type Description Style | None

    The Style to use for this highlight, or None if no style.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist class","text":"

    Bases: Exception

    Raised when the user tries to use a theme which does not exist. This means a theme which is not builtin, or has not been registered.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument","title":"WrappedDocument class","text":"
    def __init__(self, document, width=0, tab_width=4):\n

    A view into a Document which wraps the document at a certain width and can be queried to retrieve lines from the wrapped version of the document.

    Allows for incremental updates, ensuring that we only re-wrap ranges of the document that were influenced by edits.

    By default, a WrappedDocument is wrapped with width=0 (no wrapping). To wrap the document, use the wrap() method.

    Parameters Parameter Default Description document DocumentBase required

    The document to wrap.

    width int 0

    The width to wrap at.

    tab_width int 4

    The maximum width to consider for tab characters.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.document","title":"document instance-attribute","text":"
    document = document\n

    The document wrapping is performed on.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.height","title":"height property","text":"
    height: int\n

    The height of the wrapped document.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.lines","title":"lines property","text":"
    lines: list[list[str]]\n

    The lines of the wrapped version of the Document.

    Each index in the returned list represents a line index in the raw document. The list[str] at each index is the content of the raw document line split into multiple lines via wrapping.

    Note that this is expensive to compute and is not cached.

    Returns Type Description list[list[str]]

    A list of lines from the wrapped version of the document.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.wrapped","title":"wrapped property","text":"
    wrapped: bool\n

    True if the content is wrapped. This is not the same as wrapping being \"enabled\". For example, an empty document can have wrapping enabled, but no wrapping has actually occurred.

    In other words, this is True if the length of any line in the document is greater than the available width.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.get_offsets","title":"get_offsets method","text":"
    def get_offsets(self, line_index):\n

    Given a line index, get the offsets within that line where wrapping should occur for the current document.

    Parameters Parameter Default Description line_index int required

    The index of the line within the document.

    Raises Type Description ValueError

    When line_index is out of bounds.

    Returns Type Description list[int]

    The offsets within the line where wrapping should occur.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.get_sections","title":"get_sections method","text":"
    def get_sections(self, line_index):\n

    Return the sections for the given line index.

    When wrapping is enabled, a single line in the document can visually span multiple lines. The list returned represents that visually (each string in the list represents a single section (y-offset) after wrapping happens).

    Parameters Parameter Default Description line_index int required

    The index of the line to get sections for.

    Returns Type Description list[str]

    The wrapped line as a list of strings.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.get_tab_widths","title":"get_tab_widths method","text":"
    def get_tab_widths(self, line_index):\n

    Return a list of the tab widths for the given line index.

    Parameters Parameter Default Description line_index int required

    The index of the line in the document.

    Returns Type Description list[int]

    An ordered list of the expanded width of the tabs in the line.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.get_target_document_column","title":"get_target_document_column method","text":"
    def get_target_document_column(\n    self, line_index, x_offset, y_offset\n):\n

    Given a line index and the offsets within the wrapped version of that line, return the corresponding column index in the raw document.

    Parameters Parameter Default Description line_index int required

    The index of the line in the document.

    x_offset int required

    The x-offset within the wrapped line.

    y_offset int required

    The y-offset within the wrapped line (supports negative indexing).

    Returns Type Description int

    The column index corresponding to the line index and y offset.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.location_to_offset","title":"location_to_offset method","text":"
    def location_to_offset(self, location):\n

    Convert a location in the document to an offset within the wrapped/visual display of the document.

    Parameters Parameter Default Description location Location required

    The location in the document.

    Returns Type Description Offset

    The Offset in the document's visual display corresponding to the given location.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.offset_to_location","title":"offset_to_location method","text":"
    def offset_to_location(self, offset):\n

    Given an offset within the wrapped/visual display of the document, return the corresponding location in the document.

    Parameters Parameter Default Description offset Offset required

    The y-offset within the document.

    Raises Type Description ValueError

    When the given offset does not correspond to a line in the document.

    Returns Type Description Location

    The Location in the document corresponding to the given offset.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.wrap","title":"wrap method","text":"
    def wrap(self, width, tab_width=None):\n

    Wrap and cache all lines in the document.

    Parameters Parameter Default Description width int required

    The width to wrap at. 0 for no wrapping.

    tab_width int | None None

    The maximum width to consider for tab characters. If None, reuse the tab width.

    "},{"location":"widgets/text_area/#textual.document._wrapped_document.WrappedDocument.wrap_range","title":"wrap_range method","text":"
    def wrap_range(self, start, old_end, new_end):\n

    Incrementally recompute wrapping based on a performed edit.

    This must be called after the source document has been edited.

    Parameters Parameter Default Description start Location required

    The start location of the edit that was performed in document-space.

    old_end Location required

    The old end location of the edit in document-space.

    new_end Location required

    The new end location of the edit in document-space.

    "},{"location":"widgets/toast/","title":"Toast","text":"

    Added in version 0.30.0

    A widget which displays a notification message.

    • Focusable
    • Container

    Note that Toast isn't designed to be used directly in your applications, but it is instead used by notify to display a message when using Textual's built-in notification system.

    "},{"location":"widgets/toast/#styling","title":"Styling","text":"

    You can customize the style of Toasts by targeting the Toast CSS type. For example:

    Toast {\n    padding: 3;\n}\n

    The three severity levels also have corresponding classes, allowing you to target the different styles of notification. They are:

    • -information
    • -warning
    • -error

    If you wish to tailor the notifications for your application you can add rules to your CSS like this:

    Toast.-information {\n    /* Styling here. */\n}\n\nToast.-warning {\n    /* Styling here. */\n}\n\nToast.-error {\n    /* Styling here. */\n}\n

    You can customize just the title wih the toast--title class. The following would make the title italic for an information toast:

    Toast.-information .toast--title {\n    text-style: italic;\n}\n
    "},{"location":"widgets/toast/#example","title":"Example","text":"Outputtoast.py

    ToastApp \u258c\u2590 \u258cIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0\u2590 \u258cchecks\u00a0out.\u2590 \u258c\u2590 \u258c\u2590 \u258cPossible\u00a0trap\u00a0detected\u2590 \u258cNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u00a0\u2590 \u258cfully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u00a0battle\u00a0\u2590 \u258cstation!\u2590 \u258c\u2590 \u258c\u2590 \u258cIt's\u00a0a\u00a0trap!\u2590 \u258c\u2590 \u258c\u2590 \u258cIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0\u2590 \u258cimpersonate\u00a0a\u00a0deity.\u2590 \u258c\u2590

    from textual.app import App\n\n\nclass ToastApp(App[None]):\n    def on_mount(self) -> None:\n        # Show an information notification.\n        self.notify(\"It's an older code, sir, but it checks out.\")\n\n        # Show a warning. Note that Textual's notification system allows\n        # for the use of Rich console markup.\n        self.notify(\n            \"Now witness the firepower of this fully \"\n            \"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\n            title=\"Possible trap detected\",\n            severity=\"warning\",\n        )\n\n        # Show an error. Set a longer timeout so it's noticed.\n        self.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n\n        # Show an information notification, but without any sort of title.\n        self.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n\n\nif __name__ == \"__main__\":\n    ToastApp().run()\n
    "},{"location":"widgets/toast/#reactive-attributes","title":"Reactive Attributes","text":"

    This widget has no reactive attributes.

    "},{"location":"widgets/toast/#messages","title":"Messages","text":"

    This widget posts no messages.

    "},{"location":"widgets/toast/#bindings","title":"Bindings","text":"

    This widget has no bindings.

    "},{"location":"widgets/toast/#component-classes","title":"Component Classes","text":"

    The toast widget provides the following component classes:

    Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast.Toast","title":"textual.widgets._toast.Toast class","text":"
    def __init__(self, notification):\n

    Bases: Static

    A widget for displaying short-lived notifications.

    Parameters Parameter Default Description notification Notification required

    The notification to show in the toast.

    "},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {'toast--title'}\n
    Class Description toast--title Targets the title of the toast."},{"location":"widgets/tree/","title":"Tree","text":"

    Added in version 0.6.0

    A tree control widget.

    • Focusable
    • Container
    "},{"location":"widgets/tree/#example","title":"Example","text":"

    The example below creates a simple tree.

    Outputtree.py

    TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

    from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\n\n\nclass TreeApp(App):\n    def compose(self) -> ComposeResult:\n        tree: Tree[dict] = Tree(\"Dune\")\n        tree.root.expand()\n        characters = tree.root.add(\"Characters\", expand=True)\n        characters.add_leaf(\"Paul\")\n        characters.add_leaf(\"Jessica\")\n        characters.add_leaf(\"Chani\")\n        yield tree\n\n\nif __name__ == \"__main__\":\n    app = TreeApp()\n    app.run()\n

    Tree widgets have a \"root\" attribute which is an instance of a TreeNode. Call add() or add_leaf() to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.

    "},{"location":"widgets/tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/tree/#messages","title":"Messages","text":"
    • Tree.NodeCollapsed
    • Tree.NodeExpanded
    • Tree.NodeHighlighted
    • Tree.NodeSelected
    "},{"location":"widgets/tree/#bindings","title":"Bindings","text":"

    The tree widget defines the following bindings:

    Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#component-classes","title":"Component Classes","text":"

    The tree widget provides the following component classes:

    Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items.

    Make non-widget Tree support classes available.

    "},{"location":"widgets/tree/#textual.widgets.Tree","title":"textual.widgets.Tree class","text":"
    def __init__(\n    self,\n    label,\n    data=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False\n):\n

    Bases: Generic[TreeDataType], ScrollView

    A widget for displaying and navigating data in a tree.

    Parameters Parameter Default Description label TextType required

    The label of the root node of the tree.

    data TreeDataType | None None

    The optional data to associate with the root node of the tree.

    name str | None None

    The name of the Tree.

    id str | None None

    The ID of the tree in the DOM.

    classes str | None None

    The CSS classes of the tree.

    disabled bool False

    Whether the tree is disabled or not.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS: list[BindingType] = [\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"space\", \"toggle_node\", \"Toggle\", show=False),\n    Binding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\n    Binding(\n        \"down\", \"cursor_down\", \"Cursor Down\", show=False\n    ),\n]\n
    Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#textual.widgets._tree.Tree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES: set[str] = {\n    \"tree--cursor\",\n    \"tree--guides\",\n    \"tree--guides-hover\",\n    \"tree--guides-selected\",\n    \"tree--highlight\",\n    \"tree--highlight-line\",\n    \"tree--label\",\n}\n
    Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets._tree.Tree.auto_expand","title":"auto_expand class-attribute instance-attribute","text":"
    auto_expand = var(True)\n

    Auto expand tree nodes when clicked.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_line","title":"cursor_line class-attribute instance-attribute","text":"
    cursor_line = var(-1, always_update=True)\n

    The line with the cursor, or -1 if no cursor.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_node","title":"cursor_node property","text":"
    cursor_node: TreeNode[TreeDataType] | None\n

    The currently selected node, or None if no selection.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.guide_depth","title":"guide_depth class-attribute instance-attribute","text":"
    guide_depth = reactive(4, init=False)\n

    The indent depth of tree nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.hover_line","title":"hover_line class-attribute instance-attribute","text":"
    hover_line = var(-1)\n

    The line number under the mouse pointer, or -1 if not under the mouse pointer.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.last_line","title":"last_line property","text":"
    last_line: int\n

    The index of the last line.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.root","title":"root instance-attribute","text":"
    root = self._add_node(None, text_label, data)\n

    The root node of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_guides","title":"show_guides class-attribute instance-attribute","text":"
    show_guides = reactive(True)\n

    Enable display of tree guide lines.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_root","title":"show_root class-attribute instance-attribute","text":"
    show_root = reactive(True)\n

    Show the root of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed","title":"NodeCollapsed class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is collapsed.

    Can be handled using on_tree_node_collapsed in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was collapsed.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded","title":"NodeExpanded class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is expanded.

    Can be handled using on_tree_node_expanded in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was expanded.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted","title":"NodeHighlighted class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is highlighted.

    Can be handled using on_tree_node_highlighted in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was highlighted.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected","title":"NodeSelected class","text":"
    def __init__(self, node):\n

    Bases: Generic[EventTreeDataType], Message

    Event sent when a node is selected.

    Can be handled using on_tree_node_selected in a subclass of Tree or in a parent node in the DOM.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.control","title":"control property","text":"
    control: Tree[EventTreeDataType]\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.node","title":"node instance-attribute","text":"
    node: TreeNode[EventTreeDataType] = node\n

    The node that was selected.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_down","title":"action_cursor_down method","text":"
    def action_cursor_down(self):\n

    Move the cursor down one node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_up","title":"action_cursor_up method","text":"
    def action_cursor_up(self):\n

    Move the cursor up one node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_down","title":"action_page_down method","text":"
    def action_page_down(self):\n

    Move the cursor down a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_up","title":"action_page_up method","text":"
    def action_page_up(self):\n

    Move the cursor up a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_end","title":"action_scroll_end method","text":"
    def action_scroll_end(self):\n

    Move the cursor to the bottom of the tree.

    Note

    Here bottom means vertically, not branch depth.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_home","title":"action_scroll_home method","text":"
    def action_scroll_home(self):\n

    Move the cursor to the top of the tree.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_select_cursor","title":"action_select_cursor method","text":"
    def action_select_cursor(self):\n

    Cause a select event for the target node.

    Note

    If auto_expand is True use of this action on a non-leaf node will cause both an expand/collapse event to occur, as well as a selected event.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_toggle_node","title":"action_toggle_node method","text":"
    def action_toggle_node(self):\n

    Toggle the expanded state of the target node.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.clear","title":"clear method","text":"
    def clear(self):\n

    Clear all nodes under root.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_label_width","title":"get_label_width method","text":"
    def get_label_width(self, node):\n

    Get the width of the nodes label.

    The default behavior is to call render_node and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    A node.

    Returns Type Description int

    Width in cells.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_at_line","title":"get_node_at_line method","text":"
    def get_node_at_line(self, line_no):\n

    Get the node for a given line.

    Parameters Parameter Default Description line_no int required

    A line number.

    Returns Type Description TreeNode[TreeDataType] | None

    A tree node, or None if there is no node at that line.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_by_id","title":"get_node_by_id method","text":"
    def get_node_by_id(self, node_id):\n

    Get a tree node by its ID.

    Parameters Parameter Default Description node_id NodeID required

    The ID of the node to get.

    Returns Type Description TreeNode[TreeDataType]

    The node associated with that ID.

    Raises Type Description UnknownNodeID

    Raised if the TreeNode ID is unknown.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.process_label","title":"process_label method","text":"
    def process_label(self, label):\n

    Process a str or Text value into a label.

    Maybe overridden in a subclass to change how labels are rendered.

    Parameters Parameter Default Description label TextType required

    Label.

    Returns Type Description Text

    A Rich Text object.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.render_label","title":"render_label method","text":"
    def render_label(self, node, base_style, style):\n

    Render a label for the given node. Override this to modify how labels are rendered.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    A tree node.

    base_style Style required

    The base style of the widget.

    style Style required

    The additional style for the label.

    Returns Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.reset","title":"reset method","text":"
    def reset(self, label, data=None):\n

    Clear the tree and reset the root node.

    Parameters Parameter Default Description label TextType required

    The label for the root node.

    data TreeDataType | None None

    Optional data for the root node.

    Returns Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_line","title":"scroll_to_line method","text":"
    def scroll_to_line(self, line, animate=True):\n

    Scroll to the given line.

    Parameters Parameter Default Description line int required

    A line number.

    animate bool True

    Enable animation.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_node","title":"scroll_to_node method","text":"
    def scroll_to_node(self, node, animate=True):\n

    Scroll to the given node.

    Parameters Parameter Default Description node TreeNode[TreeDataType] required

    Node to scroll in to view.

    animate bool True

    Animate scrolling.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.select_node","title":"select_node method","text":"
    def select_node(self, node):\n

    Move the cursor to the given node, or reset cursor.

    Parameters Parameter Default Description node TreeNode[TreeDataType] | None required

    A tree node, or None to reset cursor.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_cursor_line","title":"validate_cursor_line method","text":"
    def validate_cursor_line(self, value):\n

    Prevent cursor line from going outside of range.

    Parameters Parameter Default Description value int required

    The value to test.

    Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_guide_depth","title":"validate_guide_depth method","text":"
    def validate_guide_depth(self, value):\n

    Restrict guide depth to reasonable range.

    Parameters Parameter Default Description value int required

    The value to test.

    Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.tree.EventTreeDataType","title":"EventTreeDataType module-attribute","text":"
    EventTreeDataType = TypeVar('EventTreeDataType')\n

    The type of the data for a given instance of a Tree.

    Similar to TreeDataType but used for Tree messages.

    "},{"location":"widgets/tree/#textual.widgets.tree.NodeID","title":"NodeID module-attribute","text":"
    NodeID = NewType('NodeID', int)\n

    The type of an ID applied to a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeDataType","title":"TreeDataType module-attribute","text":"
    TreeDataType = TypeVar('TreeDataType')\n

    The type of the data for a given instance of a Tree.

    "},{"location":"widgets/tree/#textual.widgets.tree.RemoveRootError","title":"RemoveRootError class","text":"

    Bases: Exception

    Exception raised when trying to remove the root of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"TreeNode class","text":"
    def __init__(\n    self,\n    tree,\n    parent,\n    id,\n    label,\n    data=None,\n    *,\n    expanded=True,\n    allow_expand=True\n):\n

    Bases: Generic[TreeDataType]

    An object that represents a \"node\" in a tree control.

    Parameters Parameter Default Description tree Tree[TreeDataType] required

    The tree that the node is being attached to.

    parent TreeNode[TreeDataType] | None required

    The parent node that this node is being attached to.

    id NodeID required

    The ID of the node.

    label Text required

    The label for the node.

    data TreeDataType | None None

    Optional data to associate with the node.

    expanded bool True

    Should the node be attached in an expanded state?

    allow_expand bool True

    Should the node allow being expanded by the user?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.allow_expand","title":"allow_expand writable property","text":"
    allow_expand: bool\n

    Is this node allowed to expand?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.children","title":"children property","text":"
    children: TreeNodes[TreeDataType]\n

    The child nodes of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.data","title":"data instance-attribute","text":"
    data = data\n

    Optional data associated with the tree node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.id","title":"id property","text":"
    id: NodeID\n

    The ID of the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_expanded","title":"is_expanded property","text":"
    is_expanded: bool\n

    Is the node expanded?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_last","title":"is_last property","text":"
    is_last: bool\n

    Is this the last child node of its parent?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_root","title":"is_root property","text":"
    is_root: bool\n

    Is this node the root of the tree?

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.label","title":"label writable property","text":"
    label: TextType\n

    The label for the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.line","title":"line property","text":"
    line: int\n

    The line number for this node, or -1 if it is not displayed.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.parent","title":"parent property","text":"
    parent: TreeNode[TreeDataType] | None\n

    The parent of the node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.tree","title":"tree property","text":"
    tree: Tree[TreeDataType]\n

    The tree that this node is attached to.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add","title":"add method","text":"
    def add(\n    self,\n    label,\n    data=None,\n    *,\n    expand=False,\n    allow_expand=True\n):\n

    Add a node to the sub-tree.

    Parameters Parameter Default Description label TextType required

    The new node's label.

    data TreeDataType | None None

    Data associated with the new node.

    expand bool False

    Node should be expanded.

    allow_expand bool True

    Allow use to expand the node via keyboard or mouse.

    Returns Type Description TreeNode[TreeDataType]

    A new Tree node

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add_leaf","title":"add_leaf method","text":"
    def add_leaf(self, label, data=None):\n

    Add a 'leaf' node (a node that can not expand).

    Parameters Parameter Default Description label TextType required

    Label for the node.

    data TreeDataType | None None

    Optional data.

    Returns Type Description TreeNode[TreeDataType]

    New node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse","title":"collapse method","text":"
    def collapse(self):\n

    Collapse the node (hide its children).

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse_all","title":"collapse_all method","text":"
    def collapse_all(self):\n

    Collapse the node (hide its children) and all those below it.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand","title":"expand method","text":"
    def expand(self):\n

    Expand the node (show its children).

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand_all","title":"expand_all method","text":"
    def expand_all(self):\n

    Expand the node (show its children) and all those below it.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.refresh","title":"refresh method","text":"
    def refresh(self):\n

    Initiate a refresh (repaint) of this node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove","title":"remove method","text":"
    def remove(self):\n

    Remove this node from the tree.

    Raises Type Description RemoveRootError

    If there is an attempt to remove the root.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove_children","title":"remove_children method","text":"
    def remove_children(self):\n

    Remove any child nodes of this node.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.set_label","title":"set_label method","text":"
    def set_label(self, label):\n

    Set a new label for the node.

    Parameters Parameter Default Description label TextType required

    A str or Text object with the new label.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle","title":"toggle method","text":"
    def toggle(self):\n

    Toggle the node's expanded state.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle_all","title":"toggle_all method","text":"
    def toggle_all(self):\n

    Toggle the node's expanded state and make all those below it match.

    Returns Type Description Self

    The TreeNode instance.

    "},{"location":"widgets/tree/#textual.widgets.tree.UnknownNodeID","title":"UnknownNodeID class","text":"

    Bases: Exception

    Exception raised when referring to an unknown TreeNode ID.

    "},{"location":"blog/archive/2023/","title":"2023","text":""},{"location":"blog/archive/2022/","title":"2022","text":""},{"location":"blog/category/devlog/","title":"DevLog","text":""},{"location":"blog/category/release/","title":"Release","text":""},{"location":"blog/category/news/","title":"News","text":""},{"location":"blog/page/2/","title":"Textual Blog","text":""},{"location":"blog/page/3/","title":"Textual Blog","text":""},{"location":"blog/page/4/","title":"Textual Blog","text":""},{"location":"blog/archive/2023/page/2/","title":"2023","text":""},{"location":"blog/archive/2023/page/3/","title":"2023","text":""},{"location":"blog/archive/2022/page/2/","title":"2022","text":""},{"location":"blog/category/devlog/page/2/","title":"DevLog","text":""},{"location":"blog/category/release/page/2/","title":"Release","text":""}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 1564347d8b..deb52ecc44 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,1287 +2,1302 @@ https://textual.textualize.io/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/FAQ/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/getting_started/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/help/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/roadmap/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/tutorial/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widget_gallery/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/app/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/await_complete/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/await_remove/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/binding/ - 2024-01-05 + 2024-02-01 + daily + + + https://textual.textualize.io/api/cache/ + 2024-02-01 daily https://textual.textualize.io/api/color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/command/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/containers/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/content_switcher/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/coordinate/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/dom_node/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/errors/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/events/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/filter/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/fuzzy_matcher/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/geometry/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/lazy/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/logger/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/logging/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/map_geometry/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/message/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/message_pump/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/on/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/pilot/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/query/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/reactive/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/renderables/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/screen/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/scroll_view/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/scrollbar/ - 2024-01-05 + 2024-02-01 + daily + + + https://textual.textualize.io/api/signal/ + 2024-02-01 daily https://textual.textualize.io/api/strip/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/suggester/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/system_commands_source/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/timer/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/types/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/validation/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/walk/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/widget/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/work/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/worker/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/api/worker_manager/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/12/08/be-the-keymaster/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/12/20/a-year-of-building-for-the-terminal/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/06/new-blog/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/08/version-040/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/12/11/version-060/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/24/spinners-and-progress-bars-in-textual/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/border/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/horizontal/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/integer/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/keyline/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/name/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/number/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/overflow/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/percentage/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/scalar/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/text_align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/text_style/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/css_types/vertical/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/blur/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/click/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/descendant_blur/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/descendant_focus/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/enter/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/focus/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/hide/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/key/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/leave/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/load/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mount/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_capture/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_down/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_move/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_release/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_scroll_down/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_scroll_up/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/mouse_up/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/paste/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/resize/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/screen_resume/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/screen_suspend/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/events/show/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/examples/styles/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/CSS/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/actions/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/animation/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/app/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/command_palette/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/design/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/devtools/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/events/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/input/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/layout/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/queries/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/reactivity/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/screens/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/styles/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/testing/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/widgets/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/guide/workers/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/how-to/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/how-to/center-things/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/how-to/design-a-layout/ - 2024-01-05 + 2024-02-01 + daily + + + https://textual.textualize.io/how-to/package-with-hatch/ + 2024-02-01 daily https://textual.textualize.io/how-to/render-and-compose/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/reference/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/background/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_subtitle_align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_subtitle_background/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_subtitle_color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_subtitle_style/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_title_align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_title_background/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_title_color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/border_title_style/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/box_sizing/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/content_align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/display/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/dock/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/height/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/keyline/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/layer/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/layers/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/layout/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/margin/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/max_height/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/max_width/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/min_height/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/min_width/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/offset/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/opacity/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/outline/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/overflow/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/padding/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_gutter/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_size/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/text_align/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/text_opacity/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/text_style/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/tint/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/visibility/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/width/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/column_span/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/grid_columns/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/grid_gutter/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/grid_rows/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/grid_size/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/grid/row_span/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_background/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_background_hover/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_color_hover/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_style/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/links/link_style_hover/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_active/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_hover/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_active/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_hover/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_corner_color/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/button/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/checkbox/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/collapsible/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/content_switcher/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/data_table/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/digits/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/directory_tree/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/footer/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/header/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/input/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/label/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/list_item/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/list_view/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/loading_indicator/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/log/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/markdown/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/markdown_viewer/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/option_list/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/placeholder/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/pretty/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/progress_bar/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/radiobutton/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/radioset/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/rich_log/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/rule/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/select/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/selection_list/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/sparkline/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/static/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/switch/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/tabbed_content/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/tabs/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/text_area/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/toast/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/widgets/tree/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/archive/2023/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/archive/2022/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/category/devlog/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/category/release/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/category/news/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/page/2/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/page/3/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/page/4/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/archive/2023/page/2/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/archive/2023/page/3/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/archive/2022/page/2/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/category/devlog/page/2/ - 2024-01-05 + 2024-02-01 daily https://textual.textualize.io/blog/category/release/page/2/ - 2024-01-05 + 2024-02-01 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index e727a257d7..2e330e9b43 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/styles/align/index.html b/styles/align/index.html index fbac696354..c01d5fb63f 100644 --- a/styles/align/index.html +++ b/styles/align/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6867,10 +6913,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/background/index.html b/styles/background/index.html index 285f2dc824..6e976e5dc3 100644 --- a/styles/background/index.html +++ b/styles/background/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6872,10 +6918,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border/index.html b/styles/border/index.html index b939436550..a01f87e4d6 100644 --- a/styles/border/index.html +++ b/styles/border/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,133 +6614,133 @@

    All border types + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AllBordersApp + AllBordersApp - - - - +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ -|ascii|blankdashed -+------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - -══════════════════━━━━━━━━━━━━━━━━━━ -doubleheavyhidden/none -══════════════════━━━━━━━━━━━━━━━━━━ - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -hkeyinnerouter -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - -────────────────────────────────────▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -roundsolidtall -────────────────────────────────────▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -thickvkeywide -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + ++----------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍═════════════════ +|ascii|blankdasheddouble ++----------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍═════════════════ + + + +━━━━━━━━━━━━━━━━▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +heavyhidden/nonehkeyinner +━━━━━━━━━━━━━━━━▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + + +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█████████████████───────────────────────────────── +outerpanelroundsolid +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁───────────────────────────────── + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +tallthickvkeywide +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + @@ -6721,18 +6767,19 @@

    All border types Label("hkey", id="hkey"), Label("inner", id="inner"), Label("outer", id="outer"), - Label("round", id="round"), - Label("solid", id="solid"), - Label("tall", id="tall"), - Label("thick", id="thick"), - Label("vkey", id="vkey"), - Label("wide", id="wide"), - ) - + Label("panel", id="panel"), + Label("round", id="round"), + Label("solid", id="solid"), + Label("tall", id="tall"), + Label("thick", id="thick"), + Label("vkey", id="vkey"), + Label("wide", id="wide"), + ) -if __name__ == "__main__": - app = AllBordersApp() - app.run() + +if __name__ == "__main__": + app = AllBordersApp() + app.run()

  • @@ -6772,41 +6819,45 @@

    All border types border: outer $accent; } -#round { - border: round $accent; +#panel { + border: panel $accent; } -#solid { - border: solid $accent; +#round { + border: round $accent; } -#tall { - border: tall $accent; +#solid { + border: solid $accent; } -#thick { - border: thick $accent; +#tall { + border: tall $accent; } -#vkey { - border: vkey $accent; +#thick { + border: thick $accent; } -#wide { - border: wide $accent; +#vkey { + border: vkey $accent; } -Grid { - grid-size: 3 5; - align: center middle; - grid-gutter: 1 2; -} - -Label { - width: 20; - height: 3; - content-align: center middle; -} +#wide { + border: wide $accent; +} + +Grid { + grid-size: 4 4; + align: center middle; + grid-gutter: 1 2; +} + +Label { + width: 20; + height: 3; + content-align: center middle; +}

    @@ -7167,10 +7218,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_subtitle_align/index.html b/styles/border_subtitle_align/index.html index af7e2ee634..d3e9fd2559 100644 --- a/styles/border_subtitle_align/index.html +++ b/styles/border_subtitle_align/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4816,6 +4816,8 @@ + + @@ -4952,6 +4954,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5725,6 +5748,8 @@ + + @@ -5819,6 +5844,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7003,10 +7049,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_subtitle_background/index.html b/styles/border_subtitle_background/index.html index c12dab72cb..79bbaa39a5 100644 --- a/styles/border_subtitle_background/index.html +++ b/styles/border_subtitle_background/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_subtitle_color/index.html b/styles/border_subtitle_color/index.html index 20e878778c..f59026e8b0 100644 --- a/styles/border_subtitle_color/index.html +++ b/styles/border_subtitle_color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_subtitle_style/index.html b/styles/border_subtitle_style/index.html index 9b8ab48f3d..aff8475154 100644 --- a/styles/border_subtitle_style/index.html +++ b/styles/border_subtitle_style/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_title_align/index.html b/styles/border_title_align/index.html index 078d968f5d..81a5c19f3a 100644 --- a/styles/border_title_align/index.html +++ b/styles/border_title_align/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4816,6 +4816,8 @@ + + @@ -4952,6 +4954,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5725,6 +5748,8 @@ + + @@ -5819,6 +5844,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7003,10 +7049,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_title_background/index.html b/styles/border_title_background/index.html index 8326709d7e..4833109c22 100644 --- a/styles/border_title_background/index.html +++ b/styles/border_title_background/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_title_color/index.html b/styles/border_title_color/index.html index 024c285639..75aba4e5fc 100644 --- a/styles/border_title_color/index.html +++ b/styles/border_title_color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/border_title_style/index.html b/styles/border_title_style/index.html index 9833cb98a6..050be70240 100644 --- a/styles/border_title_style/index.html +++ b/styles/border_title_style/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6568,10 +6614,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/box_sizing/index.html b/styles/box_sizing/index.html index 540cf0b652..1bc6cf7e71 100644 --- a/styles/box_sizing/index.html +++ b/styles/box_sizing/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6611,10 +6657,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/color/index.html b/styles/color/index.html index 401e74eb06..ed41976930 100644 --- a/styles/color/index.html +++ b/styles/color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6834,10 +6880,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/content_align/index.html b/styles/content_align/index.html index 1149bedb54..bf72420738 100644 --- a/styles/content_align/index.html +++ b/styles/content_align/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6864,10 +6910,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/display/index.html b/styles/display/index.html index 861ec50f10..a9323cacb3 100644 --- a/styles/display/index.html +++ b/styles/display/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6610,10 +6656,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/dock/index.html b/styles/dock/index.html index 552911e897..ba7bc51dc6 100644 --- a/styles/dock/index.html +++ b/styles/dock/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6825,10 +6871,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/column_span/index.html b/styles/grid/column_span/index.html index 62cfdb497c..2bba11b476 100644 --- a/styles/grid/column_span/index.html +++ b/styles/grid/column_span/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6586,10 +6632,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/grid_columns/index.html b/styles/grid/grid_columns/index.html index e09ce339ab..cc2f6d6e00 100644 --- a/styles/grid/grid_columns/index.html +++ b/styles/grid/grid_columns/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6578,10 +6624,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/grid_gutter/index.html b/styles/grid/grid_gutter/index.html index 0a5aaca0cc..7f41bc31be 100644 --- a/styles/grid/grid_gutter/index.html +++ b/styles/grid/grid_gutter/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4770,6 +4770,8 @@ + + @@ -4906,6 +4908,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5679,6 +5702,8 @@ + + @@ -5773,6 +5798,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6554,10 +6600,10 @@

    Python{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/grid_rows/index.html b/styles/grid/grid_rows/index.html index 5cf9bd41e4..c81b477a94 100644 --- a/styles/grid/grid_rows/index.html +++ b/styles/grid/grid_rows/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6578,10 +6624,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/grid_size/index.html b/styles/grid/grid_size/index.html index 216952ff1a..8ec6380d42 100644 --- a/styles/grid/grid_size/index.html +++ b/styles/grid/grid_size/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4794,6 +4794,8 @@ + + @@ -4930,6 +4932,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5703,6 +5726,8 @@ + + @@ -5797,6 +5822,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6797,10 +6843,10 @@

    Python{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/index.html b/styles/grid/index.html index 5abcf5dabe..689f6ce8f5 100644 --- a/styles/grid/index.html +++ b/styles/grid/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4694,6 +4694,8 @@ + + @@ -4830,6 +4832,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5603,6 +5626,8 @@ + + @@ -5697,6 +5722,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6507,10 +6553,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/grid/row_span/index.html b/styles/grid/row_span/index.html index 9f7823918d..be3c81aa61 100644 --- a/styles/grid/row_span/index.html +++ b/styles/grid/row_span/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6588,10 +6634,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/height/index.html b/styles/height/index.html index fcefcc39b2..97a4c0f6af 100644 --- a/styles/height/index.html +++ b/styles/height/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6862,10 +6908,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/index.html b/styles/index.html index df932dcffd..f1a7d9a27e 100644 --- a/styles/index.html +++ b/styles/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4692,6 +4692,8 @@ + + @@ -4828,6 +4830,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5601,6 +5624,8 @@ + + @@ -5695,6 +5720,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6200,10 +6246,10 @@

    Styles{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/keyline/index.html b/styles/keyline/index.html index ce6945c1e8..97723b160b 100644 --- a/styles/keyline/index.html +++ b/styles/keyline/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6812,10 +6858,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/layer/index.html b/styles/layer/index.html index 4da4e2f51a..922330ee9b 100644 --- a/styles/layer/index.html +++ b/styles/layer/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6569,10 +6615,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/layers/index.html b/styles/layers/index.html index 3fb6fbe9bf..daf1cd03d6 100644 --- a/styles/layers/index.html +++ b/styles/layers/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6566,10 +6612,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/layout/index.html b/styles/layout/index.html index 8299af26b7..37032bcecc 100644 --- a/styles/layout/index.html +++ b/styles/layout/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6623,10 +6669,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/index.html b/styles/links/index.html index 2004c71099..8b8ad24484 100644 --- a/styles/links/index.html +++ b/styles/links/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4694,6 +4694,8 @@ + + @@ -4830,6 +4832,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5603,6 +5626,8 @@ + + @@ -5697,6 +5722,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6502,10 +6548,10 @@

    See Also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_background/index.html b/styles/links/link_background/index.html index 7d9a543e9c..203cf76df0 100644 --- a/styles/links/link_background/index.html +++ b/styles/links/link_background/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6513,10 +6559,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_background_hover/index.html b/styles/links/link_background_hover/index.html index 3c751e5658..f64c97cd6e 100644 --- a/styles/links/link_background_hover/index.html +++ b/styles/links/link_background_hover/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4794,6 +4794,8 @@ + + @@ -4930,6 +4932,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5703,6 +5726,8 @@ + + @@ -5797,6 +5822,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6471,10 +6517,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_color/index.html b/styles/links/link_color/index.html index 869980f229..505292d5cd 100644 --- a/styles/links/link_color/index.html +++ b/styles/links/link_color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6513,10 +6559,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_color_hover/index.html b/styles/links/link_color_hover/index.html index 101f470386..33f0e4f7ea 100644 --- a/styles/links/link_color_hover/index.html +++ b/styles/links/link_color_hover/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4794,6 +4794,8 @@ + + @@ -4930,6 +4932,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5703,6 +5726,8 @@ + + @@ -5797,6 +5822,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6475,10 +6521,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_style/index.html b/styles/links/link_style/index.html index 1fa21c3e1c..5fdb67ccea 100644 --- a/styles/links/link_style/index.html +++ b/styles/links/link_style/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4794,6 +4794,8 @@ + + @@ -4930,6 +4932,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5703,6 +5726,8 @@ + + @@ -5797,6 +5822,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6542,10 +6588,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/links/link_style_hover/index.html b/styles/links/link_style_hover/index.html index 486075d51e..d597a691e8 100644 --- a/styles/links/link_style_hover/index.html +++ b/styles/links/link_style_hover/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4794,6 +4794,8 @@ + + @@ -4930,6 +4932,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5703,6 +5726,8 @@ + + @@ -5797,6 +5822,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6474,10 +6520,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/margin/index.html b/styles/margin/index.html index fc4cb58961..3696032d09 100644 --- a/styles/margin/index.html +++ b/styles/margin/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6897,10 +6943,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/max_height/index.html b/styles/max_height/index.html index b975af6cf2..797798e038 100644 --- a/styles/max_height/index.html +++ b/styles/max_height/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6583,10 +6629,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/max_width/index.html b/styles/max_width/index.html index 6be6fb30e4..1505c9ef99 100644 --- a/styles/max_width/index.html +++ b/styles/max_width/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6583,10 +6629,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/min_height/index.html b/styles/min_height/index.html index 8c90789b91..c2cca4a054 100644 --- a/styles/min_height/index.html +++ b/styles/min_height/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6585,10 +6631,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/min_width/index.html b/styles/min_width/index.html index 3f6256f55d..efece03430 100644 --- a/styles/min_width/index.html +++ b/styles/min_width/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6585,10 +6631,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/offset/index.html b/styles/offset/index.html index 803819e9cf..b27ba86c18 100644 --- a/styles/offset/index.html +++ b/styles/offset/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6583,10 +6629,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/opacity/index.html b/styles/opacity/index.html index fcb8f60e34..c5c83fdf61 100644 --- a/styles/opacity/index.html +++ b/styles/opacity/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6587,10 +6633,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/outline/index.html b/styles/outline/index.html index 0aa2aa2f75..e524f325e1 100644 --- a/styles/outline/index.html +++ b/styles/outline/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7137,10 +7183,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/overflow/index.html b/styles/overflow/index.html index 2cf4e81cd1..3b8eb4bed7 100644 --- a/styles/overflow/index.html +++ b/styles/overflow/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4783,6 +4783,8 @@ + + @@ -4919,6 +4921,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5692,6 +5715,8 @@ + + @@ -5786,6 +5811,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6607,10 +6653,10 @@

    Python{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/padding/index.html b/styles/padding/index.html index eb892c5fad..fb463d843a 100644 --- a/styles/padding/index.html +++ b/styles/padding/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6885,10 +6931,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/index.html b/styles/scrollbar_colors/index.html index b6a779478a..c699f7c992 100644 --- a/styles/scrollbar_colors/index.html +++ b/styles/scrollbar_colors/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4694,6 +4694,8 @@ + + @@ -4830,6 +4832,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5603,6 +5626,8 @@ + + @@ -5697,6 +5722,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6497,10 +6543,10 @@

    Example{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_background/index.html b/styles/scrollbar_colors/scrollbar_background/index.html index 132f15f928..929f12656c 100644 --- a/styles/scrollbar_colors/scrollbar_background/index.html +++ b/styles/scrollbar_colors/scrollbar_background/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6410,10 +6456,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_background_active/index.html b/styles/scrollbar_colors/scrollbar_background_active/index.html index c37648e7e4..0b442af9e7 100644 --- a/styles/scrollbar_colors/scrollbar_background_active/index.html +++ b/styles/scrollbar_colors/scrollbar_background_active/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6409,10 +6455,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_background_hover/index.html b/styles/scrollbar_colors/scrollbar_background_hover/index.html index beb78a34cf..2d3d039321 100644 --- a/styles/scrollbar_colors/scrollbar_background_hover/index.html +++ b/styles/scrollbar_colors/scrollbar_background_hover/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4788,6 +4788,8 @@ + + @@ -4924,6 +4926,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5697,6 +5720,8 @@ + + @@ -5791,6 +5816,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6428,10 +6474,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_color/index.html b/styles/scrollbar_colors/scrollbar_color/index.html index a4725639fe..a7d7def2a2 100644 --- a/styles/scrollbar_colors/scrollbar_color/index.html +++ b/styles/scrollbar_colors/scrollbar_color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6410,10 +6456,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_color_active/index.html b/styles/scrollbar_colors/scrollbar_color_active/index.html index d49d69d164..646ae24344 100644 --- a/styles/scrollbar_colors/scrollbar_color_active/index.html +++ b/styles/scrollbar_colors/scrollbar_color_active/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6409,10 +6455,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_color_hover/index.html b/styles/scrollbar_colors/scrollbar_color_hover/index.html index 1ab3bb9690..9093a69c02 100644 --- a/styles/scrollbar_colors/scrollbar_color_hover/index.html +++ b/styles/scrollbar_colors/scrollbar_color_hover/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6409,10 +6455,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_colors/scrollbar_corner_color/index.html b/styles/scrollbar_colors/scrollbar_corner_color/index.html index 00e5292e9e..5a83e00f05 100644 --- a/styles/scrollbar_colors/scrollbar_corner_color/index.html +++ b/styles/scrollbar_colors/scrollbar_corner_color/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4779,6 +4779,8 @@ + + @@ -4915,6 +4917,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5688,6 +5711,8 @@ + + @@ -5782,6 +5807,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6474,10 +6520,10 @@

    See also{"base": "../../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_gutter/index.html b/styles/scrollbar_gutter/index.html index 8292a9a221..2c1d150700 100644 --- a/styles/scrollbar_gutter/index.html +++ b/styles/scrollbar_gutter/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4783,6 +4783,8 @@ + + @@ -4919,6 +4921,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5692,6 +5715,8 @@ + + @@ -5786,6 +5811,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6581,10 +6627,10 @@

    Python{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/scrollbar_size/index.html b/styles/scrollbar_size/index.html index b5df54f4ba..5e62d5a95c 100644 --- a/styles/scrollbar_size/index.html +++ b/styles/scrollbar_size/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6825,10 +6871,10 @@

    Python{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/text_align/index.html b/styles/text_align/index.html index e4688de3d7..028171afe0 100644 --- a/styles/text_align/index.html +++ b/styles/text_align/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6617,10 +6663,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/text_opacity/index.html b/styles/text_opacity/index.html index a73de716ce..f6110dc4af 100644 --- a/styles/text_opacity/index.html +++ b/styles/text_opacity/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4777,6 +4777,8 @@ + + @@ -4913,6 +4915,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5686,6 +5709,8 @@ + + @@ -5780,6 +5805,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6577,10 +6623,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/text_style/index.html b/styles/text_style/index.html index 2048e368ff..de46393063 100644 --- a/styles/text_style/index.html +++ b/styles/text_style/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4792,6 +4792,8 @@ + + @@ -4928,6 +4930,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5701,6 +5724,8 @@ + + @@ -5795,6 +5820,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6797,10 +6843,10 @@

    Python{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/tint/index.html b/styles/tint/index.html index e0e6c6af58..51c4a3548c 100644 --- a/styles/tint/index.html +++ b/styles/tint/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4768,6 +4768,8 @@ + + @@ -4904,6 +4906,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5677,6 +5700,8 @@ + + @@ -5771,6 +5796,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6546,10 +6592,10 @@

    Python{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/visibility/index.html b/styles/visibility/index.html index a3327731e9..fcc9896cc8 100644 --- a/styles/visibility/index.html +++ b/styles/visibility/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4825,6 +4825,8 @@ + + @@ -4961,6 +4963,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5734,6 +5757,8 @@ + + @@ -5828,6 +5853,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6934,10 +6980,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/styles/width/index.html b/styles/width/index.html index 68bb68f246..7c33668a18 100644 --- a/styles/width/index.html +++ b/styles/width/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6864,10 +6910,10 @@

    See also{"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/tutorial/index.html b/tutorial/index.html index 8fa27bf87e..5220948394 100644 --- a/tutorial/index.html +++ b/tutorial/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4946,6 +4946,8 @@ + + @@ -5082,6 +5084,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5855,6 +5878,8 @@ + + @@ -5949,6 +5974,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6594,139 +6640,139 @@

    Stopwatch Application + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch.py + stopwatch.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:16.20 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:12.16 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:08.10 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - D  Toggle dark mode  A  Add  R  Remove  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:15.24 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:12.12 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:08.09 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + D  Toggle dark mode  A  Add  R  Remove  @@ -8176,142 +8222,142 @@

    Reactive attributes + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch05.py + stopwatch05.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.07Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.07Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.08Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - D  Toggle dark mode  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.07Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.07Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.07Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + D  Toggle dark mode  @@ -8461,146 +8507,146 @@

    Wiring buttons + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch06.py + stopwatch06.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:10.09 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:06.03 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - D  Toggle dark mode  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:10.14 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:06.07 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + D  Toggle dark mode  @@ -8748,146 +8794,146 @@

    Dynamic widgets + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch.py + stopwatch.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:06.09 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:00.00Reset - D  Toggle dark mode  A  Add  R  Remove  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:06.06 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:00.00Reset + D  Toggle dark mode  A  Add  R  Remove  @@ -9020,10 +9066,10 @@

    What next?{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widget_gallery/index.html b/widget_gallery/index.html index 4585eb501e..345b21d1f5 100644 --- a/widget_gallery/index.html +++ b/widget_gallery/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5027,6 +5027,8 @@ + + @@ -5163,6 +5165,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5936,6 +5959,8 @@ + + @@ -6030,6 +6055,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7579,138 +7625,138 @@

    DirectoryTree - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DirectoryTreeApp + DirectoryTreeApp - + - - 📂 . -┣━━ 📁 .cache -┣━━ 📁 .faq -┣━━ 📁 .git -┣━━ 📁 .github -┣━━ 📁 .ipynb_checkpoints -┣━━ 📁 .mypy_cache -┣━━ 📁 .pytest_cache -┣━━ 📁 .screenshot_cache -┣━━ 📁 .vscode -┣━━ 📁 __pycache__ -┣━━ 📁 dist▁▁ -┣━━ 📁 docs -┣━━ 📁 examples -┣━━ 📁 imgs -┣━━ 📁 notes -┣━━ 📁 questions -┣━━ 📁 reference -┣━━ 📁 sandbox -┣━━ 📁 site -┣━━ 📁 src -┣━━ 📁 tests -┣━━ 📁 tools -┣━━ 📄 .coverage + + 📂 . +┣━━ 📁 .cache +┣━━ 📁 .faq +┣━━ 📁 .git +┣━━ 📁 .github +┣━━ 📁 .ipynb_checkpoints +┣━━ 📁 .mypy_cache +┣━━ 📁 .pytest_cache +┣━━ 📁 .screenshot_cache +┣━━ 📁 .vscode +┣━━ 📁 __pycache__ +┣━━ 📁 dist▃▃ +┣━━ 📁 docs +┣━━ 📁 examples +┣━━ 📁 imgs +┣━━ 📁 notes +┣━━ 📁 questions +┣━━ 📁 reference +┣━━ 📁 sandbox +┣━━ 📁 site +┣━━ 📁 src +┣━━ 📁 tests +┣━━ 📁 tools +┣━━ 📄 .coverage @@ -8272,135 +8318,135 @@

    LoadingIndicator + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LoadingApp + LoadingApp - + - - - - - - - - - - - - -● ● ● ●  - - - - - - - - - - - + + + + + + + + + + + + +● ● ● ●  + + + + + + + + + + + @@ -8430,132 +8476,132 @@

    Log¶< font-weight: 700; } - .terminal-262632689-matrix { + .terminal-837827114-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-262632689-title { + .terminal-837827114-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-262632689-r1 { fill: #e1e1e1 } -.terminal-262632689-r2 { fill: #c5c8c6 } -.terminal-262632689-r3 { fill: #23568b } + .terminal-837827114-r1 { fill: #e1e1e1 } +.terminal-837827114-r2 { fill: #c5c8c6 } +.terminal-837827114-r3 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LogApp + LogApp - + - - And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain.▄▄ -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. + + And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain.▄▄ +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. @@ -8987,140 +9033,140 @@

    OptionList + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Aerilon -Aquaria -─────────────────────────────────────────────────── -Canceron -Caprica -─────────────────────────────────────────────────── -Gemenon -─────────────────────────────────────────────────── -Leonis -Libran -─────────────────────────────────────────────────── -Picon -─────────────────────────────────────────────────── -Sagittaron▄▄ -Scorpia -─────────────────────────────────────────────────── -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + OptionListApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Aerilon +Aquaria +────────────────────────────────────────────────── +Canceron +Caprica +────────────────────────────────────────────────── +Gemenon +────────────────────────────────────────────────── +Leonis +Libran +────────────────────────────────────────────────── +Picon▁▁ +────────────────────────────────────────────────── +Sagittaron +Scorpia +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + @@ -9636,139 +9682,139 @@

    RadioButton - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Picture - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Battlestar Galactica + Dune 1984 + Dune 2021 + Serenity + Star Trek: The Motion Picture + Star Wars: A New Hope + The Last Starfighter + Total Recall 👉 🔴 + Wing Commander +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -9798,140 +9844,140 @@

    RadioSet + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Battlestar Galactica Amanda - Dune 1984 Connor MacLeod - Dune 2021 Duncan MacLeod - Serenity Heather MacLeod - Star Trek: The Motion Picture Joe Dawson - Star Wars: A New Hope Kurgan, The - The Last Starfighter Methos - Total Recall 👉 🔴 Rachel Ellenstein - Wing Commander Ramírez -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Battlestar Galactica Amanda + Dune 1984 Connor MacLeod + Dune 2021 Duncan MacLeod + Serenity Heather MacLeod + Star Trek: The Motion Pictur Joe Dawson + Star Wars: A New Hope Kurgan, The + The Last Starfighter Methos + Total Recall 👉 🔴 Rachel Ellenstein + Wing Commander Ramírez +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -10444,139 +10490,139 @@

    SelectionList - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + SelectionListApp - - - - SelectionListApp - - - Shall we play some games? ────────────────────────────────── - -XFalken's Maze -XBlack Jack -XGin Rummy -XHearts -XBridge -XCheckers -XChess -XPoker -XFighter Combat - - - - - - -────────────────────────────────────────────────────────────── - - + + + + SelectionListApp + + + Shall we play some games? ────────────────────────────────── + +XFalken's Maze +XBlack Jack +XGin Rummy +XHearts +XBridge +XCheckers +XChess +XPoker +XFighter Combat + + + + + +────────────────────────────────────────────────────────────── + + + @@ -11282,74 +11328,72 @@

    TextArea + - + - + - + - + - + - + - + - TextAreaExample + TextAreaExample - - - - 1  defhello(name):                        -2  print("hello"+ name)    -3   -4  defgoodbye(name):           -5  print("goodbye"+ name)  -6   - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +def hello(name):                       +    print("hello" + name)  + +def goodbye(name):  +    print("goodbye" + name)  + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -11635,10 +11679,10 @@

    Tree&par - + - + diff --git a/widgets/button/index.html b/widgets/button/index.html index 9b19133dd1..3f74e81096 100644 --- a/widgets/button/index.html +++ b/widgets/button/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4939,6 +4939,8 @@ + + @@ -5075,6 +5077,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5848,6 +5871,8 @@ + + @@ -5942,6 +5967,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6996,8 +7042,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7020,8 +7066,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7790,10 +7836,10 @@

    - + - + diff --git a/widgets/checkbox/index.html b/widgets/checkbox/index.html index 01d99425c3..55298cd92e 100644 --- a/widgets/checkbox/index.html +++ b/widgets/checkbox/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4825,6 +4825,8 @@ + + @@ -4961,6 +4963,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5734,6 +5757,8 @@ + + @@ -5828,6 +5853,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6872,10 +6918,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/collapsible/index.html b/widgets/collapsible/index.html index a1ba8981a3..bad1f10582 100644 --- a/widgets/collapsible/index.html +++ b/widgets/collapsible/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4903,6 +4903,8 @@ + + @@ -5039,6 +5041,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5812,6 +5835,8 @@ + + @@ -5906,6 +5931,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7940,10 +7986,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/content_switcher/index.html b/widgets/content_switcher/index.html index 741c748c36..42d9ed57c4 100644 --- a/widgets/content_switcher/index.html +++ b/widgets/content_switcher/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6988,8 +7034,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7220,10 +7266,10 @@
    Parameters
    - + - + diff --git a/widgets/data_table/index.html b/widgets/data_table/index.html index 162858d290..f8a4b83e26 100644 --- a/widgets/data_table/index.html +++ b/widgets/data_table/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5995,6 +5995,8 @@ + + @@ -6131,6 +6133,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6904,6 +6927,8 @@ + + @@ -6998,6 +7023,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -10870,8 +10916,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -10964,8 +11010,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11039,8 +11085,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11063,8 +11109,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11087,8 +11133,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11111,8 +11157,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11158,8 +11204,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11299,8 +11345,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11323,8 +11369,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11347,8 +11393,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11371,8 +11417,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -13398,7 +13444,7 @@
    Parameters
    column_key
    - Column | str + ColumnKey | str required @@ -15519,10 +15565,10 @@

    - + - + diff --git a/widgets/digits/index.html b/widgets/digits/index.html index c7b8f8f0f6..3484de8e2b 100644 --- a/widgets/digits/index.html +++ b/widgets/digits/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4810,6 +4810,8 @@ + + @@ -4946,6 +4948,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5719,6 +5742,8 @@ + + @@ -5813,6 +5838,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6509,132 +6555,132 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ClockApp + ClockApp - + - - - - - - - - - - - - ┓  ┓    ╺━┓┏━┓   ┏━╸┏━┓ - ┃  ┃  :  ━┫┗━┫ : ┗━┓┣━┫ -╺┻╸╺┻╸   ╺━┛╺━┛   ╺━┛┗━┛ - - - - - - - - - - + + + + + + + + + + + + ┓  ┓    ╻ ╻╺━┓    ┓ ┏━┓ + ┃  ┃  : ┗━┫ ━┫ :  ┃ ┃ ┃ +╺┻╸╺┻╸     ╹╺━┛   ╺┻╸┗━┛ + + + + + + + + + + @@ -6954,10 +7000,10 @@
    Raises
    - + - + diff --git a/widgets/directory_tree/index.html b/widgets/directory_tree/index.html index eed62cc961..09df52ca2c 100644 --- a/widgets/directory_tree/index.html +++ b/widgets/directory_tree/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5002,6 +5002,8 @@ + + @@ -5138,6 +5140,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5911,6 +5934,8 @@ + + @@ -6005,6 +6030,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6719,136 +6765,137 @@

    Filtering + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DirectoryTreeApp + DirectoryTreeApp - + - - 📂 . -┣━━ 📁 __pycache__ -┣━━ 📁 dist -┣━━ 📁 docs -┣━━ 📁 examples -┣━━ 📁 imgs -┣━━ 📁 notes -┣━━ 📁 questions -┣━━ 📁 reference -┣━━ 📁 sandbox -┣━━ 📁 site -┣━━ 📁 src -┣━━ 📁 tests -┣━━ 📁 tools -┣━━ 📄 CHANGELOG.md -┣━━ 📄 CODE_OF_CONDUCT.md -┣━━ 📄 CONTRIBUTING.md -┣━━ 📄 docs.md -┣━━ 📄 faq.yml -┣━━ 📄 keys.log -┣━━ 📄 LICENSE -┣━━ 📄 Makefile -┣━━ 📄 mkdocs-common.yml -┣━━ 📄 mkdocs-nav-offline.yml + + 📂 . +┣━━ 📁 __pycache__ +┣━━ 📁 dist +┣━━ 📁 docs +┣━━ 📁 examples +┣━━ 📁 imgs +┣━━ 📁 notes +┣━━ 📁 questions +┣━━ 📁 reference +┣━━ 📁 sandbox +┣━━ 📁 site +┣━━ 📁 src +┣━━ 📁 tests +┣━━ 📁 tools +┣━━ 📄 CHANGELOG.md +┣━━ 📄 CODE_OF_CONDUCT.md +┣━━ 📄 CONTRIBUTING.md▄▄ +┣━━ 📄 docs.md +┣━━ 📄 faq.yml +┣━━ 📄 keys.log +┣━━ 📄 LICENSE +┣━━ 📄 Makefile +┣━━ 📄 mkdocs-common.yml +┣━━ 📄 mkdocs-nav-offline.yml @@ -7139,8 +7186,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7163,8 +7210,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8134,10 +8181,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/footer/index.html b/widgets/footer/index.html index fa473baf38..5c5c34e130 100644 --- a/widgets/footer/index.html +++ b/widgets/footer/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6807,10 +6853,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/header/index.html b/widgets/header/index.html index 7467022a09..f59bfcda9d 100644 --- a/widgets/header/index.html +++ b/widgets/header/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6852,8 +6898,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6994,10 +7040,10 @@

    - + - + diff --git a/widgets/index.html b/widgets/index.html index 9fbf3d11c2..c2cf5f2673 100644 --- a/widgets/index.html +++ b/widgets/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4692,6 +4692,8 @@ + + @@ -4828,6 +4830,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5601,6 +5624,8 @@ + + @@ -5695,6 +5720,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6200,10 +6246,10 @@

    Widgets{"base": "..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/input/index.html b/widgets/input/index.html index 066cf7a2de..54f53a210f 100644 --- a/widgets/input/index.html +++ b/widgets/input/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5221,6 +5221,8 @@ + + @@ -5357,6 +5359,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6130,6 +6153,8 @@ + + @@ -6224,6 +6249,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -8620,8 +8666,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8644,8 +8690,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8691,8 +8737,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8715,8 +8761,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8859,8 +8905,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9001,8 +9047,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9620,10 +9666,10 @@
    Returns
    - + - + diff --git a/widgets/label/index.html b/widgets/label/index.html index 847a3f3e8e..287c9c13b6 100644 --- a/widgets/label/index.html +++ b/widgets/label/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4786,6 +4786,8 @@ + + @@ -4922,6 +4924,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5695,6 +5718,8 @@ + + @@ -5789,6 +5814,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6592,10 +6638,10 @@

    - + - + diff --git a/widgets/list_item/index.html b/widgets/list_item/index.html index d73d65c104..857d009af0 100644 --- a/widgets/list_item/index.html +++ b/widgets/list_item/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6534,8 +6580,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6676,10 +6722,10 @@

    - + - + diff --git a/widgets/list_view/index.html b/widgets/list_view/index.html index 96b089a694..87df1d945c 100644 --- a/widgets/list_view/index.html +++ b/widgets/list_view/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4993,6 +4993,8 @@ + + @@ -5129,6 +5131,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5902,6 +5925,8 @@ + + @@ -5996,6 +6021,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7149,8 +7195,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7214,8 +7260,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7358,8 +7404,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7911,10 +7957,10 @@

    - + - + diff --git a/widgets/loading_indicator/index.html b/widgets/loading_indicator/index.html index 5aa0379c1c..664df0a6b9 100644 --- a/widgets/loading_indicator/index.html +++ b/widgets/loading_indicator/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4795,6 +4795,8 @@ + + @@ -4931,6 +4933,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5704,6 +5727,8 @@ + + @@ -5798,6 +5823,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6288,135 +6334,135 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LoadingApp + LoadingApp - + - - - - - - - - - - - - -● ● ● ●  - - - - - - - - - - - + + + + + + + + + + + + +● ● ● ●  + + + + + + + + + + + @@ -6682,10 +6728,10 @@
    Parameters
    - + - + diff --git a/widgets/log/index.html b/widgets/log/index.html index 3de0e38e2a..67dba7bae3 100644 --- a/widgets/log/index.html +++ b/widgets/log/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4900,6 +4900,8 @@ + + @@ -5036,6 +5038,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5809,6 +5832,8 @@ + + @@ -5903,6 +5928,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6503,132 +6549,132 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LogApp + LogApp - + - - And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain.▄▄ -I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings total obliteration. -I will face my fear. -I will permit it to pass over me and through me. -And when it has gone past, I will turn the inner eye to see its path. -Where the fear has gone there will be nothing. Only I will remain. + + And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain.▄▄ +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain. @@ -6848,8 +6894,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6966,8 +7012,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7447,10 +7493,10 @@
    Returns
    - + - + diff --git a/widgets/markdown/index.html b/widgets/markdown/index.html index fb266bb016..20ed1474ab 100644 --- a/widgets/markdown/index.html +++ b/widgets/markdown/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4981,6 +4981,8 @@ + + @@ -5117,6 +5119,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5890,6 +5913,8 @@ + + @@ -5984,6 +6009,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7015,8 +7061,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7882,10 +7928,10 @@
    Returns
    - + - + diff --git a/widgets/markdown_viewer/index.html b/widgets/markdown_viewer/index.html index 6e9234f5b0..03a8967884 100644 --- a/widgets/markdown_viewer/index.html +++ b/widgets/markdown_viewer/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5140,6 +5140,8 @@ + + @@ -5276,6 +5278,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6049,6 +6072,8 @@ + + @@ -6143,6 +6168,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7714,8 +7760,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8679,8 +8725,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8902,10 +8948,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/option_list/index.html b/widgets/option_list/index.html index 8eadf76669..88f25c8bde 100644 --- a/widgets/option_list/index.html +++ b/widgets/option_list/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5242,6 +5242,8 @@ + + @@ -5378,6 +5380,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6151,6 +6174,8 @@ + + @@ -6245,6 +6270,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7184,137 +7230,137 @@

    Options as simple strings + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Aerilon -Aquaria -Canceron -Caprica -Gemenon -Leonis -Libran -Picon -Sagittaron -Scorpia -Tauron -Virgon - - - - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + OptionListApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Aerilon +Aquaria +Canceron +Caprica +Gemenon +Leonis +Libran +Picon +Sagittaron +Scorpia +Tauron +Virgon + + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + @@ -7393,140 +7439,140 @@

    Options as Option instances - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Aerilon -Aquaria -─────────────────────────────────────────────────── -Canceron -Caprica -─────────────────────────────────────────────────── -Gemenon -─────────────────────────────────────────────────── -Leonis -Libran -─────────────────────────────────────────────────── -Picon -─────────────────────────────────────────────────── -Sagittaron▄▄ -Scorpia -─────────────────────────────────────────────────── -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + OptionListApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Aerilon +Aquaria +────────────────────────────────────────────────── +Canceron +Caprica +────────────────────────────────────────────────── +Gemenon +────────────────────────────────────────────────── +Leonis +Libran +────────────────────────────────────────────────── +Picon▁▁ +────────────────────────────────────────────────── +Sagittaron +Scorpia +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + @@ -7615,141 +7661,141 @@

    Options as Rich renderables + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -                 Data for Aerilon                   -┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ -Patron God   Population    Capital City   -┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩▃▃ -Demeter      1.2 Billion   Gaoth          -└───────────────┴────────────────┴────────────────┘ -                 Data for Aquaria                   -┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ -Patron God   Population   Capital City    -┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ -Hermes       75,000       None            -└───────────────┴───────────────┴─────────────────┘ -                 Data for Canceron                  -┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ -Patron God   Population    Capital City   -┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + OptionListApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +                 Data for Aerilon                  +┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ +Patron God   Population   Capital City   +┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩▇▇ +Demeter      1.2 Billion  Gaoth          +└───────────────┴───────────────┴────────────────┘ +                 Data for Aquaria                  +┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ +Patron God   Population   Capital City   +┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩ +Hermes       75,000       None           +└───────────────┴───────────────┴────────────────┘ +                Data for Canceron                  +┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ +Patron God   Population   Capital City   +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + @@ -8224,8 +8270,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8764,7 +8810,7 @@

    Raises
    - DuplicateID + DuplicateID

    If there is an attempt to use a duplicate ID.

    @@ -8847,7 +8893,7 @@

    Raises
    - DuplicateID + DuplicateID

    If there is an attempt to use a duplicate ID.

    @@ -10336,10 +10382,10 @@

    - + - + diff --git a/widgets/placeholder/index.html b/widgets/placeholder/index.html index 6d2ef4b95e..8789d71cfc 100644 --- a/widgets/placeholder/index.html +++ b/widgets/placeholder/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4819,6 +4819,8 @@ + + @@ -4955,6 +4957,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5728,6 +5751,8 @@ + + @@ -5822,6 +5847,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6757,14 +6803,14 @@

    - instance-attribute class-attribute + instance-attribute

    -
    variant: Reactive[
    -    PlaceholderVariant
    -] = self.validate_variant(variant)
    +
    variant: Reactive[PlaceholderVariant] = (
    +    self.validate_variant(variant)
    +)
     
    @@ -6965,10 +7011,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/pretty/index.html b/widgets/pretty/index.html index 308a843037..b1a2dee781 100644 --- a/widgets/pretty/index.html +++ b/widgets/pretty/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4801,6 +4801,8 @@ + + @@ -4937,6 +4939,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5710,6 +5733,8 @@ + + @@ -5804,6 +5829,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6741,10 +6787,10 @@
    Parameters
  • - + - + diff --git a/widgets/progress_bar/index.html b/widgets/progress_bar/index.html index da3e6e4e47..331cd9d500 100644 --- a/widgets/progress_bar/index.html +++ b/widgets/progress_bar/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4470,15 +4470,6 @@ -
  • - -
  • - - - compute_percentage() - - -
  • @@ -4488,33 +4479,6 @@ -
  • - -
  • - - - validate_progress() - - - -
  • - -
  • - - - validate_total() - - - -
  • - -
  • - - - watch_total() - - -
  • @@ -4930,6 +4894,8 @@ + + @@ -5066,6 +5032,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5839,6 +5826,8 @@ + + @@ -5933,6 +5922,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6453,15 +6463,6 @@ -
  • - -
  • - - - compute_percentage() - - -
  • @@ -6471,33 +6472,6 @@ -
  • - -
  • - - - validate_progress() - - - -
  • - -
  • - - - validate_total() - - - -
  • - -
  • - - - watch_total() - - -
  • @@ -8433,8 +8407,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8470,8 +8444,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8494,8 +8468,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8568,30 +8542,6 @@
    Parameters
    -

    - compute_percentage - - - method - - -

    -
    def compute_percentage(self):
    -
    - -
    - -

    Keep the percentage of progress updated automatically.

    -

    This will report a percentage of 1 if the total is zero.

    - -
    - - - -
    - - -

    update @@ -8667,75 +8617,6 @@

    Parameters
    -
    - - - -

    - validate_progress - - - method - - -

    -
    def validate_progress(self, progress):
    -
    - -
    - -

    Clamp the progress between 0 and the maximum total.

    - -
    - -
    - -
    - - - -

    - validate_total - - - method - - -

    -
    def validate_total(self, total):
    -
    - -
    - -

    Ensure the total is not negative.

    - -
    - -
    - -
    - - - -

    - watch_total - - - method - - -

    -
    def watch_total(self, total):
    -
    - -
    - -

    Re-validate progress.

    - -
    - -
    - @@ -8861,10 +8742,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/radiobutton/index.html b/widgets/radiobutton/index.html index 25738056d3..f3e879a63f 100644 --- a/widgets/radiobutton/index.html +++ b/widgets/radiobutton/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4843,6 +4843,8 @@ + + @@ -4979,6 +4981,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5752,6 +5775,8 @@ + + @@ -5846,6 +5871,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6385,139 +6431,139 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Picture - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Battlestar Galactica + Dune 1984 + Dune 2021 + Serenity + Star Trek: The Motion Picture + Star Wars: A New Hope + The Last Starfighter + Total Recall 👉 🔴 + Wing Commander +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -6701,8 +6747,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6936,10 +6982,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/radioset/index.html b/widgets/radioset/index.html index ab169ebf9a..54e66c83d7 100644 --- a/widgets/radioset/index.html +++ b/widgets/radioset/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4930,6 +4930,8 @@ + + @@ -5066,6 +5068,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5839,6 +5862,8 @@ + + @@ -5933,6 +5958,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6560,140 +6606,140 @@

    Simple example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + RadioChoicesApp - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Battlestar Galactica Amanda - Dune 1984 Connor MacLeod - Dune 2021 Duncan MacLeod - Serenity Heather MacLeod - Star Trek: The Motion Picture Joe Dawson - Star Wars: A New Hope Kurgan, The - The Last Starfighter Methos - Total Recall 👉 🔴 Rachel Ellenstein - Wing Commander Ramírez -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Battlestar Galactica Amanda + Dune 1984 Connor MacLeod + Dune 2021 Duncan MacLeod + Serenity Heather MacLeod + Star Trek: The Motion Pictur Joe Dawson + Star Wars: A New Hope Kurgan, The + The Last Starfighter Methos + Total Recall 👉 🔴 Rachel Ellenstein + Wing Commander Ramírez +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -6788,140 +6834,140 @@

    Reacting to Changes in a Radio Set - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioSetChangedApp + RadioSetChangedApp - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Battlestar Galactica - Dune 1984 - Dune 2021 - Serenity - Star Trek: The Motion Pictur - Star Wars: A New Hope - The Last Starfighter - Total Recall 👉 🔴 - Wing Commander -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▇▇ - - - - - - - -Pressed button label: Battlestar Galactica + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Battlestar Galactica + Dune 1984 + Dune 2021 + Serenity + Star Trek: The Motion Pictu + Star Wars: A New Hope + The Last Starfighter + Total Recall 👉 🔴 + Wing Commander +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▇▇ + + + + + + + +Pressed button label: Battlestar Galactica @@ -7327,8 +7373,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7643,10 +7689,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/rich_log/index.html b/widgets/rich_log/index.html index 3a4c9ebd7a..713a668951 100644 --- a/widgets/rich_log/index.html +++ b/widgets/rich_log/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4864,6 +4864,8 @@ + + @@ -5000,6 +5002,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5773,6 +5796,8 @@ + + @@ -5867,6 +5892,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6875,8 +6921,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6899,8 +6945,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6923,8 +6969,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6947,8 +6993,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6971,8 +7017,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6995,8 +7041,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7294,10 +7340,10 @@
    Returns
    - + - + diff --git a/widgets/rule/index.html b/widgets/rule/index.html index e15612e790..936b4467b6 100644 --- a/widgets/rule/index.html +++ b/widgets/rule/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4903,6 +4903,8 @@ + + @@ -5039,6 +5041,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5812,6 +5835,8 @@ + + @@ -5906,6 +5931,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7059,8 +7105,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7083,8 +7129,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7605,10 +7651,10 @@

    - + - + diff --git a/widgets/select/index.html b/widgets/select/index.html index 72d292cd7c..0e21e1b126 100644 --- a/widgets/select/index.html +++ b/widgets/select/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4993,6 +4993,8 @@ + + @@ -5129,6 +5131,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5902,6 +5925,8 @@ + + @@ -5996,6 +6021,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7653,8 +7699,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7690,8 +7736,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7714,8 +7760,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7738,8 +7784,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7762,8 +7808,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8445,10 +8491,10 @@

    - + - + diff --git a/widgets/selection_list/index.html b/widgets/selection_list/index.html index b63e845bfb..89eff133f7 100644 --- a/widgets/selection_list/index.html +++ b/widgets/selection_list/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5131,6 +5131,8 @@ + + @@ -5267,6 +5269,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6040,6 +6063,8 @@ + + @@ -6134,6 +6159,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6981,139 +7027,139 @@

    Selections as tuples + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + SelectionListApp - - - - SelectionListApp - - - Shall we play some games? ────────────────────────────────── - -XFalken's Maze -XBlack Jack -XGin Rummy -XHearts -XBridge -XCheckers -XChess -XPoker -XFighter Combat - - - - - - -────────────────────────────────────────────────────────────── - - + + + + SelectionListApp + + + Shall we play some games? ────────────────────────────────── + +XFalken's Maze +XBlack Jack +XGin Rummy +XHearts +XBridge +XCheckers +XChess +XPoker +XFighter Combat + + + + + +────────────────────────────────────────────────────────────── + + + @@ -7196,139 +7242,139 @@

    Selections as Selection objects - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + SelectionListApp - - - - SelectionListApp - - - Shall we play some games? ────────────────────────────────── - -XFalken's Maze -XBlack Jack -XGin Rummy -XHearts -XBridge -XCheckers -XChess -XPoker -XFighter Combat - - - - - - -────────────────────────────────────────────────────────────── - - + + + + SelectionListApp + + + Shall we play some games? ────────────────────────────────── + +XFalken's Maze +XBlack Jack +XGin Rummy +XHearts +XBridge +XCheckers +XChess +XPoker +XFighter Combat + + + + + +────────────────────────────────────────────────────────────── + + + @@ -7949,8 +7995,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8296,9 +8342,9 @@

    -
    selection: Selection[
    -    MessageSelectionType
    -] = selection_list.get_option_at_index(index)
    +
    selection: Selection[MessageSelectionType] = (
    +    selection_list.get_option_at_index(index)
    +)
     
    @@ -8344,9 +8390,9 @@ -
    selection_list: SelectionList[
    -    MessageSelectionType
    -] = selection_list
    +
    selection_list: SelectionList[MessageSelectionType] = (
    +    selection_list
    +)
     
    @@ -9507,10 +9553,10 @@

    - + - + diff --git a/widgets/sparkline/index.html b/widgets/sparkline/index.html index 803998b6ce..b664ad27ab 100644 --- a/widgets/sparkline/index.html +++ b/widgets/sparkline/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4852,6 +4852,8 @@ + + @@ -4988,6 +4990,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5761,6 +5784,8 @@ + + @@ -5855,6 +5880,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7808,8 +7854,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7832,8 +7878,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7976,10 +8022,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/static/index.html b/widgets/static/index.html index d0b341ca93..261fa45057 100644 --- a/widgets/static/index.html +++ b/widgets/static/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4810,6 +4810,8 @@ + + @@ -4946,6 +4948,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5719,6 +5742,8 @@ + + @@ -5813,6 +5838,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6803,10 +6849,10 @@
    Parameters
  • - + - + diff --git a/widgets/switch/index.html b/widgets/switch/index.html index 7b160d0dd6..7c1a2b7f76 100644 --- a/widgets/switch/index.html +++ b/widgets/switch/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4879,6 +4879,8 @@ + + @@ -5015,6 +5017,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5788,6 +5811,8 @@ + + @@ -5882,6 +5907,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6954,8 +7000,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -6978,8 +7024,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7285,10 +7331,10 @@
    Returns
  • - + - + diff --git a/widgets/tabbed_content/index.html b/widgets/tabbed_content/index.html index feaf7e7237..d855911cb9 100644 --- a/widgets/tabbed_content/index.html +++ b/widgets/tabbed_content/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4641,6 +4641,15 @@ +

  • + +
  • + + + active_pane + + +
  • @@ -5104,6 +5113,8 @@ + + @@ -5240,6 +5251,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6013,6 +6045,8 @@ + + @@ -6107,6 +6141,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6588,6 +6643,15 @@ +
  • + +
  • + + + active_pane + + +
  • @@ -7527,8 +7591,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -7546,6 +7610,29 @@

    + active_pane + + + + property + + +

    +
    active_pane: TabPane | None
    +
    + +
    + +

    The currently active pane, or None if no pane is active.

    +
    + + + +
    + + +

    tab_count @@ -7768,8 +7855,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -9031,10 +9118,10 @@

    {"base": "../..", "features": ["navigation.tabs", "navigation.indexes", "navigation.tabs.sticky", "navigation.footer", "content.code.annotate", "content.code.copy"], "search": "../../assets/javascripts/workers/search.f886a092.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}} + - + diff --git a/widgets/tabs/index.html b/widgets/tabs/index.html index a763560665..b4972c2e23 100644 --- a/widgets/tabs/index.html +++ b/widgets/tabs/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4916,6 +4916,15 @@ +

  • + +
  • + + + DocumentNavigator + + + + +
  • @@ -6186,6 +6471,15 @@ +
  • + +
  • + + + apply_css() + + +
  • @@ -6238,15 +6532,138 @@
  • - - - - -

  • +
  • + + + WrappedDocument + + - - - + + +
  • + + + + + +

  • + + + + @@ -6394,6 +6811,8 @@ + + @@ -6530,6 +6949,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -7303,6 +7743,8 @@ + + @@ -7397,6 +7839,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -7781,6 +8244,15 @@ +
  • + +
  • + + + Navigation and wrapping information + + +
  • @@ -8111,6 +8610,15 @@ +

  • + +
  • + + + Additional notes + + +
  • @@ -8130,6 +8638,15 @@ +
  • + +
  • + + + COMPONENT_CLASSES + + +
  • @@ -8292,6 +8809,15 @@ +
  • + +
  • + + + navigator + + +
  • @@ -8319,6 +8845,15 @@ +
  • + +
  • + + + soft_wrap + + +
  • @@ -8337,6 +8872,24 @@ +
  • + +
  • + + + wrap_width + + + +
  • + +
  • + + + wrapped_document + + +
  • @@ -8601,6 +9154,15 @@ +
  • + +
  • + + + code_editor() + + +
  • @@ -8736,15 +9298,6 @@ -
  • - -
  • - - - load_document() - - -
  • @@ -8991,6 +9544,15 @@ +
  • + +
  • + + + lines + + +
  • @@ -9062,81 +9624,267 @@
  • - + - Edit + DocumentNavigator - + +
  • + +
  • + + + Edit + + + + +
  • @@ -9381,6 +10129,15 @@ +
  • + +
  • + + + apply_css() + + +
  • @@ -9433,145 +10190,273 @@
  • - +
  • + + + WrappedDocument + + - - - - - - - -
    -
    - - - + + +
  • + + + + + + + + + + +
    +
    + + + + + + + +

    TextArea

    +
    +

    Tip

    +

    Added in version 0.38.0. Soft wrapping added in version 0.48.0.

    +
    +

    A widget for editing text which may span multiple lines. +Supports text selection, soft wrapping, optional syntax highlighting with tree-sitter +and a variety of keybindings.

    +
      +
    • Focusable
    • +
    • Container
    • +
    +

    Guide

    +

    Code editing vs plain text editing

    +

    By default, the TextArea widget is a standard multi-line input box with soft-wrapping enabled.

    +

    If you're interested in editing code, you may wish to use the [TextArea.code_editor] convenience constructor. +This is a method which, by default, returns a new TextArea with soft-wrapping disabled, line numbers enabled, and the tab key behavior configured to insert \t.

    +

    Syntax highlighting dependencies

    +

    To enable syntax highlighting, you'll need to install the syntax extra dependencies:

    +
    +
    +
    +
    pip install "textual[syntax]"
    +
    +
    +
    +
    poetry add "textual[syntax]"
    +
    +
    +
    +
    +

    This will install tree-sitter and tree-sitter-languages. +These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not available. +After installing, you can set the language reactive attribute on the TextArea to enable highlighting.

    +

    Loading text

    +

    In this example we load some initial text into the TextArea, and set the language to "python" to enable syntax highlighting.

    +
    +
    +
    +

    + + - + - + - + - + - + - + - + - + - TextAreaExample + TextAreaExample - - - - 1  defhello(name):                        -2  print("hello"+ name)    -3   -4  defgoodbye(name):           -5  print("goodbye"+ name)  -6   - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +def hello(name):                       +    print("hello" + name)  + +def goodbye(name):  +    print("goodbye" + name)  + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -9602,8 +10487,7 @@

    Loading text

    -

    To load content into the TextArea after it has already been created, -use the load_text method.

    +

    To update the content programmatically, set the text property to a string value.

    To update the parser used for syntax highlighting, set the language reactive attribute:

    # Set the language to Markdown
     text_area.language = "markdown"
    @@ -9627,7 +10511,7 @@ 

    Editing content inside TextAreaWorking with the cursor

    Moving the cursor

    The cursor location is available via the cursor_location property, which represents -the location of the cursor as a tuple (row_index, column_index). These indices are zero-based. +the location of the cursor as a tuple (row_index, column_index). These indices are zero-based and represent the position of the cursor in the content. Writing a new value to cursor_location will immediately update the location of the cursor.

    >>> text_area = TextArea()
     >>> text_area.cursor_location
    @@ -9664,74 +10548,73 @@ 

    Selecting text + - + - + - + - + - + - + - + - TextAreaSelection + TextAreaSelection - - - - 1  defhello(name): -2  print("hello"+ name) -3   -4  defgoodbye(name):           -5  print("goodbye"+ name)  -6   - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +def hello(name): +    print("hello" + name) + +def goodbye(name):  +    print("goodbye" + name)  + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -9803,6 +10686,14 @@

    ThemesDefault theme

    +

    The default TextArea theme is called css. +This a theme which takes values entirely from CSS. +This means that the default appearance of the widget fits nicely into a standard Textual application, +and looks right on both dark and light mode.

    +

    More complex applications such as code editors will likely want to use pre-defined themes such as monokai. +This involves using a TextAreaTheme object, which we cover in detail below. +This allows full customization of the TextArea, including syntax highlighting, at the code level.

    Using builtin themes

    The initial theme of the TextArea is determined by the theme parameter.

    # Create a TextArea with the 'dracula' theme.
    @@ -9845,7 +10736,8 @@ 
    1. Creating a theme)

    Attributes like cursor_style and cursor_line_style apply general language-agnostic -styling to the widget.

    +styling to the widget. +If you choose not to supply a value for one of these attributes, it will be taken from the CSS component styles.

    The syntax_styles attribute of TextAreaTheme is used for syntax highlighting and depends on the language currently in use. For more details, see syntax highlighting.

    @@ -9887,74 +10779,81 @@
    2. Registering a theme - + - + - + - + - + - + - + - + - TextAreaCustomThemes + TextAreaCustomThemes - - - - 1  # says hello -2  def hello(name):             -3      print("hello" + name)    -4   -5  # says goodbye -6  def goodbye(name):           -7      print("goodbye" + name)  -8   + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +# says hello +def hello(name):  +    print("hello" + name)  + +# says goodbye▄▄ +def goodbye(name):  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

    +

    Tab behaviour

    +

    Pressing the Tab key will shift focus to the next widget in your application by default. +This matches how other widgets work in Textual. +To have Tab insert a \t character, set the tab_behaviour attribute to the string value "indent".

    Indentation

    The character(s) inserted when you press tab is controlled by setting the indent_type attribute to either tabs or spaces.

    If indent_type == "spaces", pressing Tab will insert up to indent_width spaces in order to align with the next tab stop.

    @@ -10008,7 +10907,7 @@
    Example - closing parentheses

    This intercepts the key handler when "(" is pressed, and inserts "()" instead. It then moves the cursor so that it lands between the open and closing parentheses.

    -

    Typing def hello( into the TextArea results in the bracket automatically being closed:

    +

    Typing "def hello(" into the TextArea now results in the bracket automatically being closed:

    Example - closing parentheses font-weight: 700; } - .terminal-1282618050-matrix { + .terminal-1677876736-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1282618050-title { + .terminal-1677876736-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1282618050-r1 { fill: #c2c2bf } -.terminal-1282618050-r2 { fill: #f92672 } -.terminal-1282618050-r3 { fill: #f8f8f2 } -.terminal-1282618050-r4 { fill: #f8f8f2;font-weight: bold } -.terminal-1282618050-r5 { fill: #272822;font-weight: bold } -.terminal-1282618050-r6 { fill: #c5c8c6 } + .terminal-1677876736-r1 { fill: #1e1e1e } +.terminal-1677876736-r2 { fill: #0178d4 } +.terminal-1677876736-r3 { fill: #c5c8c6 } +.terminal-1677876736-r4 { fill: #e1e1e1 } +.terminal-1677876736-r5 { fill: #e2e2e2 } +.terminal-1677876736-r6 { fill: #e9e9e9 } +.terminal-1677876736-r7 { fill: #151515 } - + - + - + - + - TextAreaKeyPressHook + TextAreaKeyPressHook - - - - 1  def hello() - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +def hello() + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -10209,73 +11109,72 @@
    Example - adding Java support + - + - + - + - + - + - + - + - TextAreaCustomLanguage + TextAreaCustomLanguage - - - - 1  class HelloWorld {                                -2  publicstatic void main(String[] args) {  -3          System.out.println("Hello, World!");  -4      }                                         -5  }                                             -6   - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +class HelloWorld {                               +    public static void main(String[] args) {  +        System.out.println("Hello, World!");  +    }  + + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -10291,6 +11190,13 @@
    Example - adding Java supportNavigation and wrapping information
    +

    If you're building functionality on top of TextArea, it may be useful to inspect the navigator and wrapped_document attributes.

    +
      +
    • navigator is a DocumentNavigator instance which can give us general information about the cursor's location within a document, as well as where the cursor will move to when certain actions are performed.
    • +
    • wrapped_document is a WrappedDocument instance which can be used to convert document locations to visual locations, taking wrapping into account. It also offers a variety of other convenience methods and properties.
    • +
    +

    A detailed view of these classes is out of scope, but do note that a lot of the functionality of TextArea exists within them, so inspecting them could be worthwhile.

    Reactive attributes

    @@ -10323,7 +11229,7 @@

    Reactive attributesReactive attributesMessages

    @@ -10486,16 +11398,68 @@

    BindingsComponent classes

    -

    The TextArea widget defines no component classes.

    -

    Styling should be done exclusively via TextAreaTheme.

    -

    See also

    +

    The TextArea defines component classes that can style various aspects of the widget. +Styles from the theme attribute take priority.

    + + +
    + + + +
    + +

    TextArea offers some component classes which can be used to style aspects of the widget.

    +

    Note that any attributes provided in the chosen TextAreaTheme will take priority here.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ClassDescription
    text-area--cursorTarget the cursor.
    text-area--gutterTarget the gutter (line number column).
    text-area--cursor-gutterTarget the gutter area of the line the cursor is on.
    text-area--cursor-lineTarget the line the cursor is on.
    text-area--selectionTarget the current selection.
    text-area--matching-bracketTarget matching brackets.
    +
    + +

    See also

    +

    Additional notes

    +
      +
    • To remove the outline effect when the TextArea is focused, you can set border: none; padding: 0; in your CSS.
    • +

    @@ -10520,11 +11484,14 @@

    *, language=None, theme=None, - name=None, - id=None, - classes=None, - disabled=False -): + soft_wrap=True, + tab_behaviour="focus", + show_line_numbers=False, + name=None, + id=None, + classes=None, + disabled=False +):

    @@ -10577,6 +11544,39 @@
    Parameters

    The theme to use.

    + + + soft_wrap +
    + bool + + + True + +

    Enable soft wrapping.

    + + + + tab_behaviour +
    + Literal['focus', 'indent'] + + + 'focus' + +

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    + + + + show_line_numbers +
    + bool + + + False + +

    Show line numbers on the left edge.

    + name @@ -10644,8 +11644,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -10926,6 +11926,71 @@

    +

    + COMPONENT_CLASSES + + + + class-attribute + + +

    +
    COMPONENT_CLASSES: set[str] = {
    +    "text-area--cursor",
    +    "text-area--gutter",
    +    "text-area--cursor-gutter",
    +    "text-area--cursor-line",
    +    "text-area--selection",
    +    "text-area--matching-bracket",
    +}
    +
    + +
    + +

    TextArea offers some component classes which can be used to style aspects of the widget.

    +

    Note that any attributes provided in the chosen TextAreaTheme will take priority here.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ClassDescription
    text-area--cursorTarget the cursor.
    text-area--gutterTarget the gutter (line number column).
    text-area--cursor-gutterTarget the gutter area of the line the cursor is on.
    text-area--cursor-lineTarget the line the cursor is on.
    text-area--selectionTarget the current selection.
    text-area--matching-bracketTarget matching brackets.
    +
    + +
    + +
    + + +

    available_languages @@ -11127,12 +12192,12 @@

    -
    cursor_blink: Reactive[bool] = reactive(True)
    +
    cursor_blink: Reactive[bool] = reactive(True, init=False)
     
    @@ -11151,8 +12216,8 @@

    - property writable + property

    @@ -11286,12 +12351,12 @@

    - instance-attribute class-attribute + instance-attribute

    -
    indent_width: Reactive[int] = reactive(4)
    +
    indent_width: Reactive[int] = reactive(4, init=False)
     
    @@ -11335,8 +12400,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11363,12 +12428,14 @@

    - instance-attribute class-attribute + instance-attribute

    -
    match_cursor_bracket: Reactive[bool] = reactive(True)
    +
    match_cursor_bracket: Reactive[bool] = reactive(
    +    True, init=False
    +)
     
    @@ -11382,6 +12449,32 @@

    + navigator + + + + instance-attribute + + +

    +
    navigator: DocumentNavigator = DocumentNavigator(
    +    self.wrapped_document
    +)
    +
    + +
    + +

    Queried to determine where the cursor should move given a navigation +action, accounting for wrapping etc.

    +
    + +
    + +
    + + +

    selected_text @@ -11410,13 +12503,13 @@

    - instance-attribute class-attribute + instance-attribute

    selection: Reactive[Selection] = reactive(
    -    Selection(), always_update=True, init=False
    +    Selection(), init=False, always_update=True
     )
     
    @@ -11440,12 +12533,14 @@

    - instance-attribute class-attribute + instance-attribute

    -
    show_line_numbers: Reactive[bool] = reactive(True)
    +
    show_line_numbers: Reactive[bool] = reactive(
    +    False, init=False
    +)
     
    @@ -11460,13 +12555,37 @@

    + soft_wrap + + + + class-attribute + instance-attribute + + +

    +
    soft_wrap: Reactive[bool] = reactive(True, init=False)
    +
    + +
    + +

    True if text should soft wrap.

    +
    + +
    + +
    + + +

    text - property writable + property

    @@ -11489,8 +12608,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -11506,6 +12625,55 @@

    +
    + + + +

    + wrap_width + + + + property + + +

    +
    wrap_width: int
    +
    + +
    + +

    The width which gets used when the document wraps.

    +

    Accounts for gutter, scrollbars, etc.

    +
    + +
    + +
    + + + +

    + wrapped_document + + + + instance-attribute + + +

    +
    wrapped_document: WrappedDocument = WrappedDocument(
    +    self.document
    +)
    +
    + +
    + +

    The wrapped view of the document.

    +
    + +
    +
    @@ -12428,6 +13596,167 @@

    +

    + code_editor + + + classmethod + + +

    +
    def code_editor(
    +    cls,
    +    text="",
    +    *,
    +    language=None,
    +    theme="monokai",
    +    soft_wrap=False,
    +    tab_behaviour="indent",
    +    show_line_numbers=True,
    +    name=None,
    +    id=None,
    +    classes=None,
    +    disabled=False
    +):
    +
    + +
    + +

    Construct a new TextArea with sensible defaults for editing code.

    +

    This instantiates a TextArea with line numbers enabled, soft wrapping +disabled, and "indent" tab behaviour.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + text +
    + str +
    + '' +

    The initial text to load into the TextArea.

    + language +
    + str | None +
    + None +

    The language to use.

    + theme +
    + str | None +
    + 'monokai' +

    The theme to use.

    + soft_wrap +
    + bool +
    + False +

    Enable soft wrapping.

    + tab_behaviour +
    + Literal['focus', 'indent'] +
    + 'indent' +

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    + show_line_numbers +
    + bool +
    + True +

    Show line numbers on the left edge.

    + name +
    + str | None +
    + None +

    The name of the TextArea widget.

    + id +
    + str | None +
    + None +

    The ID of the widget, used to refer to it from Textual CSS.

    + classes +
    + str | None +
    + None +

    One or more Textual CSS compatible class names separated by spaces.

    + disabled +
    + bool +
    + False +

    True if the widget is disabled.

    + +
    + +
    + +
    + + +

    delete @@ -12568,13 +13897,13 @@

    Returns
    - Any + EditResult

    Data relating to the edit that may be useful. The data returned

    - Any + EditResult

    may be different depending on the edit performed.

    @@ -12878,13 +14207,39 @@

    -
    def get_cursor_line_start_location(self):
    +
    def get_cursor_line_start_location(self, smart_home=False):
     

    Get the location of the start of the current line.

    +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + smart_home +
    + bool +
    + False +

    If True, use "smart home key" behavior - go to the first +non-whitespace character on the line, and if already there, go to +offset 0. Smart home only works when wrapping is disabled.

    +
    Returns
    @@ -13312,55 +14667,8 @@
    Returns
    -

    - load_document - - - method - - -

    -
    def load_document(self, document):
    -
    - -
    - -

    Load a document into the TextArea.

    - -
    Parameters
    -
    - - - - - - - - - - - - - - -
    ParameterDefaultDescription
    - document -
    - DocumentBase -
    - required -

    The document to load into the TextArea.

    - -
    - -
    - -
    - - - -

    - load_text +

    + load_text method @@ -13515,7 +14823,7 @@

    -

    Move the cursor relative to its current location.

    +

    Move the cursor relative to its current location in document-space.

    Parameters
    @@ -14530,6 +15838,7 @@

    Replace text at the given range.

    +

    This is the only method by which a document may be updated.

    Parameters
    @@ -14654,8 +15963,8 @@

    - abstractmethod property + abstractmethod

    @@ -14673,13 +15982,40 @@

    + lines + + + + property + abstractmethod + + +

    +
    lines: list[str]
    +
    + +
    + +

    Get the lines of the document as a list of strings.

    +

    The strings should not include newline characters. The newline +character used for the document can be retrieved via the newline +property.

    +
    + + + +
    + + +

    newline - abstractmethod property + abstractmethod

    @@ -14702,8 +16038,8 @@

    - abstractmethod property + abstractmethod

    @@ -15121,22 +16457,81 @@
    Returns
    -

    - Edit +

    + DocumentNavigator class -

    - + +
    def __init__(self, wrapped_document):
    +
    -

    Implements the Undoable protocol to replace text at some range within a document.

    +

    Cursor navigation in the TextArea is "wrapping-aware".

    +

    Although the cursor location (the selection) is represented as a location +in the raw document, when you actually move the cursor, it must take wrapping +into account (otherwise things start to look really confusing to the user where +wrapping is involved).

    +

    Your cursor visually moves through the wrapped version of the document, rather +than the raw document. So, for example, pressing down on the keyboard +may move your cursor to a position further along the current raw document line, +rather than on to the next line in the raw document.

    +

    The DocumentNavigator class manages that behaviour.

    +

    Given a cursor location in the unwrapped document, and a cursor movement action, +this class can inform us of the destination the cursor will move to considering +the current wrapping width and document content. It can also translate between +document-space (a location/(row,col) in the raw document), and visual-space +(x and y offsets) as the user will see them on screen after the document has been +wrapped.

    +

    For this to work correctly, the wrapped_document and document must be synchronised. +This means that if you make an edit to the document, you must then update the +wrapped document, and then you may query the document navigator.

    +

    Naming conventions:

    +

    A "location" refers to a location, in document-space (in the raw document). It +is entirely unrelated to visually positioning. A location in a document can appear +in any visual position, as it is influenced by scrolling, wrapping, gutter settings, +and the cell width of characters to its left.

    +

    A "wrapped section" refers to a portion of the line accounting for wrapping. +For example the line "ABCDEF" when wrapped at width 3 will result in 2 sections: +"ABC" and "DEF". In this case, we call "ABC" is the first section/wrapped section.

    +

    A "wrap offset" is an integer representing the index at which wrapping occurs in a +document-space line. This is a codepoint index, rather than a visual offset. +In "ABCDEF" with wrapping at width 3, there is a single wrap offset of 3.

    +

    "Smart home" refers to a modification of the "home" key behaviour. If smart home is +enabled, the first non-whitespace character is considered to be the home location. +If the cursor is currently at this position, then the normal home behaviour applies. +This is designed to make cursor movement more useful to end users.

    + + +
    Parameters
    +
    + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + wrapped_document +
    + WrappedDocument +
    + required +

    The WrappedDocument to be used when making navigation decisions.

    @@ -15153,114 +16548,112 @@

    -

    - from_location +

    + last_x_offset instance-attribute -

    -
    from_location: Location
    +

    +
    last_x_offset = 0
     
    -

    The start location of the insert.

    +

    Remembers the last x offset (cell width) the cursor was moved horizontally to, +so that it can be restored on vertical movement where possible.

    -
    - - - -

    - maintain_selection_offset - - - - instance-attribute - - -

    -
    maintain_selection_offset: bool
    -
    - -
    - -

    If True, the selection will maintain its offset to the replacement range.

    -
    - -
    -
    +
    -

    - text +

    + clamp_reachable - instance-attribute + method -

    -
    text: str
    +

    +
    def clamp_reachable(self, location):
     
    -

    The text to insert. An empty string is equivalent to deletion.

    -
    - - +

    Given a location, return the nearest location that corresponds to a +reachable location in the document.

    -
    - - - -

    - to_location - - - - instance-attribute - +

    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    A location.

    -
  • -
    to_location: Location
    -
    +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Location +

    The nearest reachable location in the document.

    -
    - -

    The end location of the insert

    - -
    -

    - after +

    + get_location_above method -

    -
    def after(self, text_area):
    +

    +
    def get_location_above(self, location):
     
    -

    Possibly update the cursor location after the widget has been refreshed.

    +

    Get the location visually aligned with the cell above the given location.

    Parameters
    @@ -15274,14 +16667,32 @@
    Parameters
    - + + + +
    - text_area + location
    - TextArea + Location
    required

    The TextArea this operation was performed on.

    The location to start from.

    + +
    Returns
    + + + + + + + + + + +
    TypeDescription
    + Location +

    The cell above the given location.

    @@ -15294,20 +16705,20 @@
    Parameters
    -

    - do +

    + get_location_at_y_offset method -

    -
    def do(self, text_area):
    +
  • +
    def get_location_at_y_offset(self, location, vertical_offset):
     
    -

    Perform the edit operation.

    +

    Apply a visual vertical offset to a location and check the resulting location.

    Parameters
    @@ -15321,14 +16732,25 @@
    Parameters
    - + + + + + +
    - text_area + location
    - TextArea + Location
    required

    The TextArea to perform the edit on.

    The location to start from.

    + vertical_offset +
    + int +
    + required +

    The vertical offset to move (negative=up, positive=down).

    @@ -15344,9 +16766,9 @@
    Returns
    - EditResult + Location -

    An EditResult containing information about the replace operation.

    +

    The location after the offset has been applied.

    @@ -15359,20 +16781,22 @@
    Returns
    -

    - undo +

    + get_location_below method -

    -
    def undo(self, text_area):
    +
  • +
    def get_location_below(self, location):
     
    -

    Undo the edit operation.

    +

    Given a location in the raw document, return the raw document +location corresponding to moving down in the wrapped representation +of the document.

    Parameters
    @@ -15386,14 +16810,14 @@
    Parameters
    - +
    - text_area + location
    - TextArea + Location
    required

    The TextArea to undo the insert operation on.

    The location in the raw document.

    @@ -15409,9 +16833,9 @@
    Returns
    - EditResult + Location -

    An EditResult containing information about the replace operation.

    +

    The location which is visually below the given location.

    @@ -15420,98 +16844,1963 @@
    Returns
    +
    -
    - +

    + get_location_end + + + method + - +

    +
    def get_location_end(self, location):
    +
    -
    +
    + +

    Get the location corresponding to the end of the current section.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The current location.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Location +

    The location corresponding to the end of the wrapped line.

    +
    +
    +
    -

    - EditResult +

    + get_location_home - class + method -

    - +
  • +
    def get_location_home(self, location, smart_home=False):
    +
    - -

    Contains information about an edit that has occurred.

    +

    Get the "home location" corresponding to the given location.

    +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to consider.

    + smart_home +
    + bool +
    + False +

    Enable/disable 'smart home' behaviour.

    +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Location +

    The home location, relative to the given location.

    - +
    -
    +
    +
    +

    + get_location_left + + + method + +

    +
    def get_location_left(self, location):
    +
    +
    + +

    Get the location to the left of the given location.

    +

    Note that if the given location is at the start of the line, then +this will return the end of the preceding line, since that's where +you would expect the cursor to move.

    -
    +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to start from.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Location +

    The location to the right.

    + +
    +
    +
    -

    - end_location + +

    + get_location_right - instance-attribute + method -

    -
    end_location: Location
    +
  • +
    def get_location_right(self, location):
     
    -

    The new end Location after the edit is complete.

    +

    Get the location to the right of the given location.

    +

    Note that if the given location is at the end of the line, then +this will return the start of the following line, since that's where +you would expect the cursor to move.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to start from.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Location +

    The location to the right.

    +
    -
    - +
    -

    - replaced_text +

    + is_end_of_document - instance-attribute + method -

    -
    replaced_text: str
    -
    +
  • +
    def is_end_of_document(self, location):
    +
    + +
    + +

    Check if a location is at the end of the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is at the end of the document.

    + +
    + + + +
    + + + +

    + is_end_of_document_line + + + method + + +

    +
    def is_end_of_document_line(self, location):
    +
    + +
    + +

    True if the location is at the end of a line in the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the document is on the last line of the document.

    + +
    + +
    + +
    + + + +

    + is_end_of_wrapped_line + + + method + + +

    +
    def is_end_of_wrapped_line(self, location):
    +
    + +
    + +

    True if the location is at the end of a wrapped line.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is on the last wrapped section of any line.

    + +
    + +
    + +
    + + + +

    + is_first_document_line + + + method + + +

    +
    def is_first_document_line(self, location):
    +
    + +
    + +

    Check if the given location is on the first line in the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is on the first line of the document.

    + +
    + +
    + +
    + + + +

    + is_first_wrapped_line + + + method + + +

    +
    def is_first_wrapped_line(self, location):
    +
    + +
    + +

    Check if the given location is on the first wrapped section of the first line in the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is on the first wrapped section of the first line.

    + +
    + +
    + +
    + + + +

    + is_last_document_line + + + method + + +

    +
    def is_last_document_line(self, location):
    +
    + +
    + +

    Check if the given location is on the last line of the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True when the location is on the last line of the document.

    + +
    + +
    + +
    + + + +

    + is_last_wrapped_line + + + method + + +

    +
    def is_last_wrapped_line(self, location):
    +
    + +
    + +

    Check if the given location is on the last wrapped section of the last line.

    +

    That is, the cursor is visually on the last rendered row.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is on the last section of the last line.

    + +
    + +
    + +
    + + + +

    + is_start_of_document + + + method + + +

    +
    def is_start_of_document(self, location):
    +
    + +
    + +

    Check if a location is at the start of the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to examine.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if and only if the cursor is at document location (0, 0)

    + +
    + +
    + +
    + + + +

    + is_start_of_document_line + + + method + + +

    +
    def is_start_of_document_line(self, location):
    +
    + +
    + +

    True when the location is at the start of the first document line.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to check.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if the location is at column index 0.

    + +
    + +
    + +
    + + + +

    + is_start_of_wrapped_line + + + method + + +

    +
    def is_start_of_wrapped_line(self, location):
    +
    + +
    + +

    True when the location is at the start of the first wrapped line.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to check.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + bool +

    True if the location is at column index 0.

    + +
    + +
    + + + + + + + + + +
    + + + + + +

    + Edit + + + + class + + +

    + + +
    + + +

    Implements the Undoable protocol to replace text at some range within a document.

    + + + + + +
    + + + + + + + +
    + + + +

    + from_location + + + + instance-attribute + + +

    +
    from_location: Location
    +
    + +
    + +

    The start location of the insert.

    +
    + +
    + +
    + + + +

    + maintain_selection_offset + + + + instance-attribute + + +

    +
    maintain_selection_offset: bool
    +
    + +
    + +

    If True, the selection will maintain its offset to the replacement range.

    +
    + +
    + +
    + + + +

    + text + + + + instance-attribute + + +

    +
    text: str
    +
    + +
    + +

    The text to insert. An empty string is equivalent to deletion.

    +
    + +
    + +
    + + + +

    + to_location + + + + instance-attribute + + +

    +
    to_location: Location
    +
    + +
    + +

    The end location of the insert

    +
    + +
    + + + +
    + + + +

    + after + + + method + + +

    +
    def after(self, text_area):
    +
    + +
    + +

    Possibly update the cursor location after the widget has been refreshed.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + text_area +
    + TextArea +
    + required +

    The TextArea this operation was performed on.

    + +
    + +
    + +
    + + + +

    + do + + + method + + +

    +
    def do(self, text_area):
    +
    + +
    + +

    Perform the edit operation.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + text_area +
    + TextArea +
    + required +

    The TextArea to perform the edit on.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + EditResult +

    An EditResult containing information about the replace operation.

    + +
    + +
    + +
    + + + +

    + undo + + + method + + +

    +
    def undo(self, text_area):
    +
    + +
    + +

    Undo the edit operation.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + text_area +
    + TextArea +
    + required +

    The TextArea to undo the insert operation on.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + EditResult +

    An EditResult containing information about the replace operation.

    + +
    + +
    + + + +
    + +
    + +
    + +
    + + + + + +

    + EditResult + + + + class + + +

    + + +
    + + +

    Contains information about an edit that has occurred.

    + + + + + +
    + + + + + + + +
    + + + +

    + end_location + + + + instance-attribute + + +

    +
    end_location: Location
    +
    + +
    + +

    The new end Location after the edit is complete.

    +
    + +
    + +
    + + + +

    + replaced_text + + + + instance-attribute + + +

    +
    replaced_text: str
    +
    + +
    + +

    The text that was replaced.

    +
    + +
    + + + + + +
    + +
    + +
    + +
    + + + + + +

    + LanguageDoesNotExist + + + + class + + +

    + + +
    +

    + Bases: Exception

    + + +

    Raised when the user tries to use a language which does not exist. +This means a language which is not builtin, or has not been registered.

    + + + +
    + +
    + +
    + + + + + +

    + Selection + + + + class + + +

    + + +
    +

    + Bases: NamedTuple

    + + +

    A range of characters within a document from a start point to the end point. +The location of the cursor is always considered to be the end point of the selection. +The selection is inclusive of the minimum point and exclusive of the maximum point.

    + + + + + +
    + + + + + + + +
    + + + +

    + end + + + + class-attribute + instance-attribute + + +

    +
    end: Location = (0, 0)
    +
    + +
    + +

    The end location of the selection.

    +

    If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

    +
    + +
    + +
    + + + +

    + is_empty + + + + property + + +

    +
    is_empty: bool
    +
    + +
    + +

    Return True if the selection has 0 width, i.e. it's just a cursor.

    +
    + +
    + +
    + + + +

    + start + + + + class-attribute + instance-attribute + + +

    +
    start: Location = (0, 0)
    +
    + +
    + +

    The start location of the selection.

    +

    If you were to click and drag a selection inside a text-editor, this is where you started dragging.

    +
    + +
    + + + +
    + + + +

    + cursor + + + classmethod + + +

    +
    def cursor(cls, location):
    +
    + +
    + +

    Create a Selection with the same start and end point - a "cursor".

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + location +
    + Location +
    + required +

    The location to create the zero-width Selection.

    + +
    + +
    + + + +
    + +
    + +
    + +
    + + + + + +

    + SyntaxAwareDocument + + + + class + + +

    +
    def __init__(self, text, language):
    +
    + +
    +

    + Bases: Document

    + + +

    A wrapper around a Document which also maintains a tree-sitter syntax +tree when the document is edited.

    +

    The primary reason for this split is actually to keep tree-sitter stuff separate, +since it isn't supported in Python 3.7. By having the tree-sitter code +isolated in this subclass, it makes it easier to conditionally import. However, +it does come with other design flaws (e.g. Document is required to have methods +which only really make sense on SyntaxAwareDocument).

    +

    If you're reading this and Python 3.7 is no longer supported by Textual, +consider merging this subclass into the Document superclass.

    + + + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + text +
    + str +
    + required +

    The initial text contained in the document.

    + language +
    + str | Language +
    + required +

    The language to use. You can pass a string to use a supported +language, or pass in your own tree-sitter Language object.

    + + + + +
    + + + + + + + +
    + + + +

    + language + + + + instance-attribute + + +

    +
    language: Language | None = get_language(language)
    +
    + +
    + +

    The tree-sitter Language or None if tree-sitter is unavailable.

    +
    + +
    + + + +
    + + + +

    + get_line + + + method + + +

    +
    def get_line(self, line_index):
    +
    + +
    + +

    Return the string representing the line, not including new line characters.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + line_index +
    + int +
    + required +

    The index of the line.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + str +

    The string representing the line.

    + +
    + +
    + +
    + + + +

    + prepare_query + + + method + + +

    +
    def prepare_query(self, query):
    +
    + +
    + +

    Prepare a tree-sitter tree query.

    +

    Queries should be prepared once, then reused.

    +

    To execute a query, call query_syntax_tree.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + query +
    + str +
    + required +

    The string query to prepare.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + Query | None +

    The prepared query.

    + +
    + +
    + +
    + + + +

    + query_syntax_tree + + + method + + +

    +
    def query_syntax_tree(
    +    self, query, start_point=None, end_point=None
    +):
    +
    + +
    + +

    Query the tree-sitter syntax tree.

    +

    The default implementation always returns an empty list.

    +

    To support querying in a subclass, this must be implemented.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + query +
    + Query +
    + required +

    The tree-sitter Query to perform.

    + start_point +
    + tuple[int, int] | None +
    + None +

    The (row, column byte) to start the query at.

    + end_point +
    + tuple[int, int] | None +
    + None +

    The (row, column byte) to end the query at.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + list[tuple['Node', str]] +

    A tuple containing the nodes and text captured by the query.

    + +
    + +
    + +
    + + + +

    + replace_range + + + method + + +

    +
    def replace_range(self, start, end, text):
    +
    + +
    + +

    Replace text at the given range.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + start +
    + Location +
    + required +

    A tuple (row, column) where the edit starts.

    + end +
    + Location +
    + required +

    A tuple (row, column) where the edit ends.

    + text +
    + str +
    + required +

    The text to insert between start and end.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + EditResult +

    The new end location after the edit is complete.

    -
    - -

    The text that was replaced.

    - -
    @@ -15524,90 +18813,91 @@

    - LanguageDoesNotExist +

    + TextAreaTheme class -

    +
  • -

    - Bases: Exception

    -

    Raised when the user tries to use a language which does not exist. -This means a language which is not builtin, or has not been registered.

    +

    A theme for the TextArea widget.

    +

    Allows theming the general widget (gutter, selections, cursor, and so on) and +mapping of tree-sitter tokens to Rich styles.

    +

    For example, consider the following snippet from the markdown.scm highlight +query file. We've assigned the heading_content token type to the name heading.

    +
    (heading_content) @heading
    +
    +

    Now, we can map this heading name to a Rich style, and it will be styled as +such in the TextArea, assuming a parser which returns a heading_content +node is used (as will be the case when language="markdown").

    +
    TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})
    +
    +

    We can register this theme with our TextArea using the TextArea.register_theme method, +and headings in our markdown files will be styled bold cyan.

    -
    + - +
    -
    -

    - Selection - - - class - +
    -

    -
    -

    - Bases: NamedTuple

    +

    + base_style -

    A range of characters within a document from a start point to the end point. -The location of the cursor is always considered to be the end point of the selection. -The selection is inclusive of the minimum point and exclusive of the maximum point.

    - + + class-attribute + instance-attribute + +

    +
    base_style: Style | None = None
    +
    +
    +

    The background style of the text area. If None the parent style will be used.

    +
    -
    - - - - - - +
    -

    - end +

    + bracket_matching_style - instance-attribute class-attribute + instance-attribute -

    -
    end: Location = (0, 0)
    +
  • +
    bracket_matching_style: Style | None = None
     
    -

    The end location of the selection.

    -

    If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

    +

    The style to apply to matching brackets. If None, a legible Style will be generated.

    @@ -15616,21 +18906,23 @@

    -

    - is_empty +

    + cursor_line_gutter_style - property + class-attribute + instance-attribute -

    -
    is_empty: bool
    +
  • +
    cursor_line_gutter_style: Style | None = None
     
    -

    Return True if the selection has 0 width, i.e. it's just a cursor.

    +

    The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be +generated.

    @@ -15639,185 +18931,143 @@

    -

    - start +

    + cursor_line_style - instance-attribute class-attribute + instance-attribute -

    -
    start: Location = (0, 0)
    +
  • +
    cursor_line_style: Style | None = None
     
    -

    The start location of the selection.

    -

    If you were to click and drag a selection inside a text-editor, this is where you started dragging.

    +

    The style to apply to the line the cursor is on.

    +
    -
    - +

    + cursor_style -

    - cursor - classmethod + class-attribute + instance-attribute -

    -
    def cursor(cls, location):
    +
  • +
    cursor_style: Style | None = None
     
    -

    Create a Selection with the same start and end point - a "cursor".

    - -
    Parameters
    - - - - - - - - - - - - - - - -
    ParameterDefaultDescription
    - location -
    - Location -
    - required -

    The location to create the zero-width Selection.

    - -
    - - - - - - - +

    The style of the cursor. If None, a legible Style will be generated.

    -
    - - +
    -

    - SyntaxAwareDocument +

    + gutter_style - class + class-attribute + instance-attribute -

    -
    def __init__(self, text, language):
    +
  • +
    gutter_style: Style | None = None
     
    -

    - Bases: Document

    - -

    A wrapper around a Document which also maintains a tree-sitter syntax -tree when the document is edited.

    -

    The primary reason for this split is actually to keep tree-sitter stuff separate, -since it isn't supported in Python 3.7. By having the tree-sitter code -isolated in this subclass, it makes it easier to conditionally import. However, -it does come with other design flaws (e.g. Document is required to have methods -which only really make sense on SyntaxAwareDocument).

    -

    If you're reading this and Python 3.7 is no longer supported by Textual, -consider merging this subclass into the Document superclass.

    +

    The style of the gutter. If None, a legible Style will be generated.

    +
    - + -
    Parameters
    - - - - - - - - - - - - - - - - - - - - -
    ParameterDefaultDescription
    - text -
    - str -
    - required -

    The initial text contained in the document.

    - language -
    - str | Language -
    - required -

    The language to use. You can pass a string to use a supported -language, or pass in your own tree-sitter Language object.

    +
    + + + +

    + name + + + + instance-attribute + +

    +
    name: str
    +
    +
    +

    The name of the theme.

    +
    + +
    + +
    -
    +

    + selection_style + + + class-attribute + instance-attribute + +

    +
    selection_style: Style | None = None
    +
    +
    + +

    The style of the selection. If None a default selection Style will be generated.

    +
    +
    -

    - language +

    + syntax_styles + class-attribute instance-attribute -

    -
    language: Language | None = None
    +
  • +
    syntax_styles: dict[str, Style] = field(
    +    default_factory=dict
    +)
     
    -

    The tree-sitter Language or None if tree-sitter is unavailable.

    +

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    @@ -15828,20 +19078,23 @@

    - get_line +

    + apply_css method -

    -
    def get_line(self, line_index):
    +
  • +
    def apply_css(self, text_area):
     
    -

    Return the string representing the line, not including new line characters.

    +

    Apply CSS rules from a TextArea to be used for fallback styling.

    +

    If any attributes in the theme aren't supplied, they'll be filled with the appropriate +base CSS (e.g. color, background, etc.) and component CSS (e.g. text-area--cursor) from +the supplied TextArea.

    Parameters
    @@ -15855,18 +19108,41 @@
    Parameters
    - +
    - line_index + text_area
    - int + TextArea
    required

    The index of the line.

    The TextArea instance to retrieve fallback styling from.

    +
    + + + +
    + + + +

    + builtin_themes + + + classmethod + + +

    +
    def builtin_themes(cls):
    +
    + +
    + +

    Get a list of all builtin TextAreaThemes.

    +
    Returns
    @@ -15878,9 +19154,9 @@
    Returns
    - +
    - str + list[TextAreaTheme]

    The string representing the line.

    A list of all builtin TextAreaThemes.

    @@ -15893,46 +19169,20 @@
    Returns
    -

    - prepare_query +

    + default - method + classmethod -

    -
    def prepare_query(self, query):
    +
  • +
    def default(cls):
     
    -

    Prepare a tree-sitter tree query.

    -

    Queries should be prepared once, then reused.

    -

    To execute a query, call query_syntax_tree.

    - -
    Parameters
    - - - - - - - - - - - - - - - -
    ParameterDefaultDescription
    - query -
    - str -
    - required -

    The string query to prepare.

    +

    Get the default syntax theme.

    Returns
    @@ -15945,9 +19195,9 @@
    Returns
    - +
    - Query | None + TextAreaTheme

    The prepared query.

    The default TextAreaTheme (probably "css").

    @@ -15960,24 +19210,21 @@
    Returns
    -

    - query_syntax_tree +

    + get_builtin_theme - method + classmethod -

    -
    def query_syntax_tree(
    -    self, query, start_point=None, end_point=None
    -):
    +
  • +
    def get_builtin_theme(cls, theme_name):
     
    -

    Query the tree-sitter syntax tree.

    -

    The default implementation always returns an empty list.

    -

    To support querying in a subclass, this must be implemented.

    +

    Get a TextAreaTheme by name.

    +

    Given a theme_name, return the corresponding TextAreaTheme object.

    Parameters
    @@ -15991,36 +19238,14 @@
    Parameters
    - - - - - - - - - - - +
    - query + theme_name
    - Query + str
    required

    The tree-sitter Query to perform.

    - start_point -
    - tuple[int, int] | None -
    - None -

    The (row, column byte) to start the query at.

    - end_point -
    - tuple[int, int] | None -
    - None -

    The (row, column byte) to end the query at.

    The name of the theme.

    @@ -16036,9 +19261,10 @@
    Returns
    - list[tuple['Node', str]] + TextAreaTheme | None -

    A tuple containing the nodes and text captured by the query.

    +

    The TextAreaTheme corresponding to the name or None if the theme isn't +found.

    @@ -16051,20 +19277,21 @@
    Returns
    -

    - replace_range +

    + get_highlight method -

    -
    def replace_range(self, start, end, text):
    +

    +
    def get_highlight(self, name):
     
    -

    Replace text at the given range.

    +

    Return the Rich style corresponding to the name defined in the tree-sitter +highlight query for the current theme.

    Parameters
    @@ -16078,36 +19305,14 @@
    Parameters
    - - - - - - - - - - - +
    - start -
    - Location -
    - required -

    A tuple (row, column) where the edit starts.

    - end -
    - Location -
    - required -

    A tuple (row, column) where the edit ends.

    - text + name
    str
    required

    The text to insert between start and end.

    The name of the highlight.

    @@ -16123,9 +19328,9 @@
    Returns
    - EditResult + Style | None -

    The new end location after the edit is complete.

    +

    The Style to use for this highlight, or None if no style.

    @@ -16136,152 +19341,150 @@
    Returns
    -
    - - - - - -
    - - - - - -

    - TextAreaTheme - - - - class - - -

    - - -
    - - -

    A theme for the TextArea widget.

    -

    Allows theming the general widget (gutter, selections, cursor, and so on) and -mapping of tree-sitter tokens to Rich styles.

    -

    For example, consider the following snippet from the markdown.scm highlight -query file. We've assigned the heading_content token type to the name heading.

    -
    (heading_content) @heading
    -
    -

    Now, we can map this heading name to a Rich style, and it will be styled as -such in the TextArea, assuming a parser which returns a heading_content -node is used (as will be the case when language="markdown").

    -
    TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})
    -
    -

    We can register this theme with our TextArea using the TextArea.register_theme method, -and headings in our markdown files will be styled bold cyan.

    - - - - - -
    - - +
    +
    +
    +
    -
    -

    - base_style +

    + ThemeDoesNotExist - instance-attribute - class-attribute + class -

    -
    base_style: Style | None = None
    -
    +

    +
    +

    + Bases: Exception

    + -

    The background style of the text area. If None the parent style will be used.

    +

    Raised when the user tries to use a theme which does not exist. +This means a theme which is not builtin, or has not been registered.

    + + +
    -
    +
    + -

    - bracket_matching_style + +

    + WrappedDocument - instance-attribute - class-attribute + class -

    -
    bracket_matching_style: Style | None = None
    +

    +
    def __init__(self, document, width=0, tab_width=4):
     
    + -

    The style to apply to matching brackets. If None, a legible Style will be generated.

    -
    +

    A view into a Document which wraps the document at a certain +width and can be queried to retrieve lines from the wrapped version +of the document.

    +

    Allows for incremental updates, ensuring that we only re-wrap ranges of the document +that were influenced by edits.

    - + +

    By default, a WrappedDocument is wrapped with width=0 (no wrapping). +To wrap the document, use the wrap() method.

    -
    +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + document +
    + DocumentBase +
    + required +

    The document to wrap.

    + width +
    + int +
    + 0 +

    The width to wrap at.

    + tab_width +
    + int +
    + 4 +

    The maximum width to consider for tab characters.

    + + +
    + -

    - cursor_line_gutter_style - - - instance-attribute - class-attribute - -

    -
    cursor_line_gutter_style: Style | None = None
    -
    -
    - -

    The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be -generated.

    -
    -
    -

    - cursor_line_style +

    + document instance-attribute - class-attribute -

    -
    cursor_line_style: Style | None = None
    +

    +
    document = document
     
    -

    The style to apply to the line the cursor is on.

    +

    The document wrapping is performed on.

    @@ -16290,22 +19493,21 @@

    - cursor_style +

    + height - instance-attribute - class-attribute + property -

    -
    cursor_style: Style | None = None
    +

    +
    height: int
     
    -

    The style of the cursor. If None, a legible Style will be generated.

    +

    The height of the wrapped document.

    @@ -16314,22 +19516,43 @@

    - gutter_style +

    + lines - instance-attribute - class-attribute + property -

    -
    gutter_style: Style | None = None
    +

    +
    lines: list[list[str]]
     
    -

    The style of the gutter. If None, a legible Style will be generated.

    +

    The lines of the wrapped version of the Document.

    +

    Each index in the returned list represents a line index in the raw +document. The list[str] at each index is the content of the raw document line +split into multiple lines via wrapping.

    +

    Note that this is expensive to compute and is not cached.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + list[list[str]] +

    A list of lines from the wrapped version of the document.

    @@ -16338,95 +19561,225 @@

    - name +

    + wrapped - instance-attribute + property -

    -
    name: str
    +

    +
    wrapped: bool
     
    -

    The name of the theme.

    +

    True if the content is wrapped. This is not the same as wrapping being "enabled". +For example, an empty document can have wrapping enabled, but no wrapping has actually +occurred.

    +

    In other words, this is True if the length of any line in the document is greater +than the available width.

    -
    +
    + -

    - selection_style +

    + get_offsets - instance-attribute - class-attribute + method -

    -
    selection_style: Style | None = None
    +

    +
    def get_offsets(self, line_index):
     
    -

    The style of the selection. If None a default selection Style will be generated.

    +

    Given a line index, get the offsets within that line where wrapping +should occur for the current document.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + line_index +
    + int +
    + required +

    The index of the line within the document.

    + +
    Raises
    + + + + + + + + + + + + + +
    TypeDescription
    + ValueError +

    When line_index is out of bounds.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + list[int] +

    The offsets within the line where wrapping should occur.

    +
    -
    - +
    -

    - syntax_styles +

    + get_sections - instance-attribute - class-attribute + method -

    -
    syntax_styles: dict[str, Style] = field(
    -    default_factory=dict
    -)
    +
  • +
    def get_sections(self, line_index):
     
    -
    - -

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    +
    + +

    Return the sections for the given line index.

    +

    When wrapping is enabled, a single line in the document can visually span +multiple lines. The list returned represents that visually (each string in +the list represents a single section (y-offset) after wrapping happens).

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + line_index +
    + int +
    + required +

    The index of the line to get sections for.

    + +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    + list[str] +

    The wrapped line as a list of strings.

    +
    - -
    -

    - builtin_themes +

    + get_tab_widths - classmethod + method -

    -
    def builtin_themes(cls):
    +

    +
    def get_tab_widths(self, line_index):
     
    -

    Get a list of all builtin TextAreaThemes.

    +

    Return a list of the tab widths for the given line index.

    + +
    Parameters
    + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + line_index +
    + int +
    + required +

    The index of the line in the document.

    Returns
    @@ -16439,9 +19792,9 @@
    Returns
    - +
    - list[TextAreaTheme] + list[int]

    A list of all builtin TextAreaThemes.

    An ordered list of the expanded width of the tabs in the line.

    @@ -16454,20 +19807,69 @@
    Returns
    -

    - default +

    + get_target_document_column - classmethod + method -

    -
    def default(cls):
    +

    +
    def get_target_document_column(
    +    self, line_index, x_offset, y_offset
    +):
     
    -

    Get the default syntax theme.

    +

    Given a line index and the offsets within the wrapped version of that +line, return the corresponding column index in the raw document.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + line_index +
    + int +
    + required +

    The index of the line in the document.

    + x_offset +
    + int +
    + required +

    The x-offset within the wrapped line.

    + y_offset +
    + int +
    + required +

    The y-offset within the wrapped line (supports negative indexing).

    Returns
    @@ -16480,9 +19882,9 @@
    Returns
    - +
    - TextAreaTheme + int

    The default TextAreaTheme (probably "monokai").

    The column index corresponding to the line index and y offset.

    @@ -16495,21 +19897,20 @@
    Returns
    -

    - get_builtin_theme +

    + location_to_offset - classmethod + method -

    -
    def get_builtin_theme(cls, theme_name):
    +

    +
    def location_to_offset(self, location):
     
    -

    Get a TextAreaTheme by name.

    -

    Given a theme_name, return the corresponding TextAreaTheme object.

    +

    Convert a location in the document to an offset within the wrapped/visual display of the document.

    Parameters
    @@ -16523,14 +19924,14 @@
    Parameters
    - +
    - theme_name + location
    - str + Location
    required

    The name of the theme.

    The location in the document.

    @@ -16546,10 +19947,9 @@
    Returns
    - TextAreaTheme | None + Offset -

    The TextAreaTheme corresponding to the name or None if the theme isn't -found.

    +

    The Offset in the document's visual display corresponding to the given location.

    @@ -16562,21 +19962,21 @@
    Returns
    -

    - get_highlight +

    + offset_to_location method -

    -
    def get_highlight(self, name):
    +

    +
    def offset_to_location(self, offset):
     
    -

    Return the Rich style corresponding to the name defined in the tree-sitter -highlight query for the current theme.

    +

    Given an offset within the wrapped/visual display of the document, +return the corresponding location in the document.

    Parameters
    @@ -16590,14 +19990,33 @@
    Parameters
    - + + + +
    - name + offset
    - str + Offset
    required

    The name of the highlight.

    The y-offset within the document.

    + +
    Raises
    + + + + + + + + + + +
    TypeDescription
    + ValueError +

    When the given offset does not correspond to a line +in the document.

    @@ -16613,9 +20032,9 @@
    Returns
    - Style | None + Location -

    The Style to use for this highlight, or None if no style.

    +

    The Location in the document corresponding to the given offset.

    @@ -16624,41 +20043,139 @@
    Returns
    +
    -
    - +

    + wrap + + + method + - +

    +
    def wrap(self, width, tab_width=None):
    +
    -
    +
    + +

    Wrap and cache all lines in the document.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + width +
    + int +
    + required +

    The width to wrap at. 0 for no wrapping.

    + tab_width +
    + int | None +
    + None +

    The maximum width to consider for tab characters. If None, +reuse the tab width.

    +
    +
    +
    -

    - ThemeDoesNotExist +

    + wrap_range - class + method -

    - +
  • +
    def wrap_range(self, start, old_end, new_end):
    +
    -

    - Bases: Exception

    - -

    Raised when the user tries to use a theme which does not exist. -This means a theme which is not builtin, or has not been registered.

    +

    Incrementally recompute wrapping based on a performed edit.

    +

    This must be called after the source document has been edited.

    + +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterDefaultDescription
    + start +
    + Location +
    + required +

    The start location of the edit that was performed in document-space.

    + old_end +
    + Location +
    + required +

    The old end location of the edit in document-space.

    + new_end +
    + Location +
    + required +

    The new end location of the edit in document-space.

    + +
    + + + + @@ -16789,10 +20306,10 @@

    - + - + diff --git a/widgets/toast/index.html b/widgets/toast/index.html index 6d45831f74..0d5bf2ccf1 100644 --- a/widgets/toast/index.html +++ b/widgets/toast/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -4810,6 +4810,8 @@ + + @@ -4946,6 +4948,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -5719,6 +5742,8 @@ + + @@ -5813,6 +5838,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -6355,139 +6401,139 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToastApp + ToastApp - - - - - - - -It's an older code, sir, but it  -checks out. - - - -Possible trap detected -Now witness the firepower of this -fully ARMED and OPERATIONAL -battle station! - - - -It's a trap! - - - -It's against my programming to  -impersonate a deity. - + + + + + + + +It's an older code, sir, but it  +checks out. + + + +Possible trap detected +Now witness the firepower of this  +fully ARMED and OPERATIONAL battle  +station! + + + +It's a trap! + + + +It's against my programming to  +impersonate a deity. + @@ -6785,10 +6831,10 @@

    - + - + diff --git a/widgets/tree/index.html b/widgets/tree/index.html index e5f6c948d6..3638b109cc 100644 --- a/widgets/tree/index.html +++ b/widgets/tree/index.html @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -5479,6 +5479,8 @@ + + @@ -5615,6 +5617,27 @@ +
  • + + + + + Cache + + + + +
  • + + + + + + + + + +
  • @@ -6388,6 +6411,8 @@ + + @@ -6482,6 +6507,27 @@ +
  • + + + + + Package a Textual app with Hatch + + + + +
  • + + + + + + + + + +
  • @@ -8200,8 +8246,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8224,8 +8270,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8271,8 +8317,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8295,8 +8341,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8365,8 +8411,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -8389,8 +8435,8 @@

    - instance-attribute class-attribute + instance-attribute

    @@ -10006,8 +10052,8 @@

    - property writable + property

    @@ -10168,8 +10214,8 @@

    - property writable + property

    @@ -10979,10 +11025,10 @@

    - + - +