diff --git a/api/app/index.html b/api/app/index.html index f2ebab053d..93b680179c 100644 --- a/api/app/index.html +++ b/api/app/index.html @@ -5049,6 +5049,15 @@ + + +
  • + + +  ALLOW_IN_MAXIMIZED_VIEW + + +
  • @@ -9411,6 +9420,15 @@ +
  • + +
  • + + +  ALLOW_IN_MAXIMIZED_VIEW + + +
  • @@ -12572,6 +12590,29 @@

    + + + +

    + ALLOW_IN_MAXIMIZED_VIEW + + + + class-attribute + + +

    +
    ALLOW_IN_MAXIMIZED_VIEW = 'Footer'
    +
    + +
    + +

    The default value of Screen.ALLOW_IN_MAXIMIZED_VIEW.

    +
    + + +
    diff --git a/api/geometry/index.html b/api/geometry/index.html index 223300bcec..f715ef68eb 100644 --- a/api/geometry/index.html +++ b/api/geometry/index.html @@ -5697,6 +5697,57 @@ +
  • + +
  • + + +  constrain + + + + +
  • @@ -6558,6 +6609,24 @@ +
  • + +
  • + + +  max_height + + + +
  • + +
  • + + +  max_width + + +
  • @@ -8316,6 +8385,57 @@ +
  • + +
  • + + +  constrain + + + + +
  • @@ -9177,6 +9297,24 @@ +
  • + +
  • + + +  max_height + + + +
  • + +
  • + + +  max_width + + +
  • @@ -10777,6 +10915,127 @@

    +

    + constrain + + +

    +
    constrain(constrain_x, constrain_y, margin, container)
    +
    + +
    + +

    Constrain a region to fit within a container, using different methods per axis.

    + + +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    +

    constrain_x +

    + Literal['none', 'inside', 'inflect'] + +
    +

    Constrain method for the X-axis.

    +
    +
    + required +
    +

    constrain_y +

    + Literal['none', 'inside', 'inflect'] + +
    +

    Constrain method for the Y-axis.

    +
    +
    + required +
    +

    margin +

    + Spacing + +
    +

    Margin to maintain around region.

    +
    +
    + required +
    +

    container +

    + Region + +
    +

    Container to constrain to.

    +
    +
    + required +
    + + +

    Returns:

    + + + + + + + + + + + + + +
    TypeDescription
    + Region + +
    +

    New widget, that fits inside the container (if possible).

    +
    +
    + +
    + + + +
    + +

    contains @@ -11732,19 +11991,20 @@

    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.

    +

    If a margin is provided, it will add space between the resulting region.

    +

    Note that if margin is specified it overlaps, so the space will be the maximum +of two edges, and not the total.

    ╔══════════╗    │
     ║          ║
     ║   Self   ║    │
     ║          ║
     ╚══════════╝    │
     
    -─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─
    -
    -                │    ┌──────────┐
    -                     │          │
    -                │    │  Result  │
    -                     │          │
    -                │    └──────────┘
    +─ ─ ─ ─ ─ ─ ─ ─ ┌──────────┐
    +                │          │
    +                │  Result  │
    +                │          │
    +                └──────────┘
     
    @@ -13360,6 +13620,52 @@

    +

    + max_height + + + + property + + +

    +
    max_height
    +
    + +
    + +

    The space between regions in the Y direction if margins overlap, i.e. max(self.top, self.bottom).

    +
    + +
    + +
    + + + +

    + max_width + + + + property + + +

    +
    max_width
    +
    + +
    + +

    The space between regions in the X direction if margins overlap, i.e. max(self.left, self.right).

    +
    + +
    + +
    + + +

    right diff --git a/api/pilot/index.html b/api/pilot/index.html index 7d12f2c009..7b5bc4e22c 100644 --- a/api/pilot/index.html +++ b/api/pilot/index.html @@ -5482,9 +5482,9 @@
    • - + -  selector +  widget @@ -5566,9 +5566,9 @@
      • - + -  selector +  widget @@ -5599,9 +5599,9 @@
        • - + -  selector +  widget @@ -5659,9 +5659,9 @@
          • - + -  selector +  widget @@ -6814,9 +6814,9 @@
            • - + -  selector +  widget @@ -6898,9 +6898,9 @@
              • - + -  selector +  widget @@ -6931,9 +6931,9 @@
                • - + -  selector +  widget @@ -6991,9 +6991,9 @@
                  • - + -  selector +  widget @@ -7302,7 +7302,7 @@

                    click(
                    -    selector=None,
                    +    widget=None,
                         offset=(0, 0),
                         shift=False,
                         meta=False,
                    @@ -7338,14 +7338,14 @@ 

                    -

                    selector -

                    +

                    widget +

                    - type[Widget] | str | None + Widget | type[Widget] | str | None
                    -

                    A selector to specify a widget that should be used as the reference +

                    A widget or selector used as an origin 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 @@ -7365,7 +7365,7 @@

                    -

                    The offset to click. The offset is relative to the selector provided +

                    The offset to click. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

                    @@ -7541,7 +7541,7 @@

                    -
                    hover(selector=None, offset=(0, 0))
                    +
                    hover(widget=None, offset=(0, 0))
                     
                    @@ -7564,14 +7564,14 @@

                    -

                    selector -

                    +

                    widget +

                    - type[Widget] | str | None | None + Widget | type[Widget] | str | None | None
                    -

                    A selector to specify a widget that should be used as the reference +

                    A widget or selector used as an origin 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 @@ -7591,7 +7591,7 @@

                    -

                    The offset to hover. The offset is relative to the selector provided +

                    The offset to hover. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

                    @@ -7666,7 +7666,7 @@

                    mouse_down(
                    -    selector=None,
                    +    widget=None,
                         offset=(0, 0),
                         shift=False,
                         meta=False,
                    @@ -7694,14 +7694,14 @@ 

                    -

                    selector -

                    +

                    widget +

                    - type[Widget] | str | None + Widget | type[Widget] | str | None
                    -

                    A selector to specify a widget that should be used as the reference +

                    A widget or selector used as an origin 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 @@ -7721,7 +7721,7 @@

                    -

                    The offset for the event. The offset is relative to the selector +

                    The offset for the event. The offset is relative to the selector / widget provided or to the screen, if no selector is provided.

                    @@ -7844,7 +7844,7 @@

                    mouse_up(
                    -    selector=None,
                    +    widget=None,
                         offset=(0, 0),
                         shift=False,
                         meta=False,
                    @@ -7872,14 +7872,14 @@ 

                    -

                    selector -

                    +

                    widget +

                    - type[Widget] | str | None + Widget | type[Widget] | str | None
                    -

                    A selector to specify a widget that should be used as the reference +

                    A widget or selector used as an origin 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 @@ -7899,7 +7899,7 @@

                    -

                    The offset for the event. The offset is relative to the selector +

                    The offset for the event. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

                    diff --git a/api/screen/index.html b/api/screen/index.html index 0e1f765358..6b4433c28f 100644 --- a/api/screen/index.html +++ b/api/screen/index.html @@ -8361,12 +8361,13 @@

                    -
                    ALLOW_IN_MAXIMIZED_VIEW = '.-textual-system,Footer'
                    +
                    ALLOW_IN_MAXIMIZED_VIEW = None
                     
                    -

                    A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget).

                    +

                    A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget). Or +None to default to App.ALLOW_IN_MAXIMIZED_VIEW

                    diff --git a/api/widget/index.html b/api/widget/index.html index 29912cbd4d..9a1541a8c7 100644 --- a/api/widget/index.html +++ b/api/widget/index.html @@ -5907,6 +5907,15 @@ +
                  • + +
                  • + + +  absolute_offset + + +
                  • @@ -9888,6 +9897,15 @@ +
                  • + +
                  • + + +  absolute_offset + + +
                  • @@ -13566,6 +13584,29 @@

                    +

                    + absolute_offset + + + + instance-attribute + + +

                    +
                    absolute_offset = None
                    +
                    + +
                    + +

                    Force an absolute offset for the widget (used by tooltips).

                    +
                    + +

    + +
    + + +

    allow_horizontal_scroll diff --git a/blog/images/compositor/cuts.excalidraw.svg b/blog/images/compositor/cuts.excalidraw.svg new file mode 100644 index 0000000000..454ac2943c --- /dev/null +++ b/blog/images/compositor/cuts.excalidraw.svg @@ -0,0 +1,21 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlTXHUwMDFiSVx1MDAxMv3uX+Hwflx1MDAxZHqyqrKyKidiY1x1MDAwM7BcdTAwMDFzmfvamHBcYqlBXHK6kJpDTMx/3yyBUSPRtlxi00Y9i1x1MDAxY1x1MDAwMabPUtV7mS+P7r/evX//Ie134lx1MDAwZn+8/1x1MDAxMN9UK42k1q1cXH/4LWy/iru9pN2SXXrwd6992a1cdTAwMGWOrKdpp/fH7783K93zOO00KtU4ukp6l5VGL72sJe2o2m7+nqRxs/ef8HO90oz/3Wk3a2k3XHUwMDFh3mQmriVpu3t3r7hcdTAwMTE341bak6v/V/5+//6vwc/M6LpxNa20Tlx1MDAxYvHghMGu4Vx1MDAwMFmPblxcb7dcdTAwMDZjJfCOjXvYnfQ+yr3SuCb7TmS88XBP2PShf9m4XFyurK5XXHUwMDEzz8manbPzt43l4S1PkkZjO+03XHUwMDA2Q6p2273eTL2SVuvDI3ppt31cdTAwMWXvJ7W0/m3mMttcdTAwMWbO7bVlXHUwMDE2hmd125en9VbcXHUwMDBiXHUwMDEzoFx1MDAxZba2O5VqkvbDNoCHrXez8Mf74ZZcdTAwMWL5y4CLXGZbrzR7r+WXfdhcdTAwMWQugFx1MDAwNiNw1lx1MDAwMuDo7ruRzbdcdTAwMWKyXHUwMDE4MrJ/qTj8XHUwMDFiju24Uj0/lVx1MDAwMbZqw2MqtuZPTobHXFzff19cdTAwMDJcdTAwMTOBUoDOkFx0t3k4olx1MDAxZSen9TRMiY88XCJqZTw55V1mIPFgWdiy08hsXHUwMDFldoS7dz7XXHUwMDA26PhzuFx1MDAxNl3B1edwRuuy0chOZat2P5XfUDTEkbnf8vfw64XjP2XwN7zDZadWuYOKctpcdTAwMDNcdTAwMTnWXHUwMDFhLD3sbySt89HbN9rV8yG63mXu9TxIW0V5mFZeXHUwMDE5kPVmnlx1MDAxONafPn/d2FrWfDW7sLG/u7wxn9RXtspcdTAwMDFrXHUwMDFmkVx1MDAxMvSyIatccuIjVFx1MDAxYqdcIq2UdkaxMZbxp0B9rE/08fE4qI2FyFvnXHUwMDE0OmByPsOdIaopQlJibfxcdTAwMWSqcVx1MDAxNNVKeGfM1ILa/Vx1MDAxMlBzXHUwMDA2siOgRvCWlfV2YkxfxfHlXHT2t2ut7mm10j1Z+rL2tV9cdTAwMDZMo+fIM1x1MDAxMFx1MDAxM4mJtMz0XHUwMDE41FxuXCJcdTAwMWJmQ0xccjpW2vxcdTAwMTSqT06qXFzlolCtyFtAXHUwMDAw9U/HtZz+JKQp30xcdTAwMGKcZS3BqIkhvXfe3OhcdTAwMWSbjcOjzY1afLo7O1x1MDAwYpdHXHUwMDA1QLpW6dXjXHUwMDE3ttNcdTAwMWOhXHUwMDEyW+wh2GpF7lx1MDAxMaa1o0iJOkEn62KdN8XIXHUwMDBmiIyWUbDMuVx1MDAwNa3tU5hW3ovPMKDZilxiQSY/XG5qrcDKyeC5XGJQ619cdTAwMGLqzDxXuulcXNKqJa3T0VPiVi1nT6PSS+fbzWaSyjA22kkrXHUwMDFkPWJw3dlut31djytjcyFXzt3XXHSXXHUwMDFiSv7wXHUwMDE5/u/9XHUwMDEwYIM/XHUwMDFl/v/nb09cdTAwMWU9k7/24TO+6sNcdTAwMGK+y/5+Lv+VtaNbXHUwMDFmXFxcdTAwMWEhsnYwuUu7ZuzHa3p9e+X0Y/W0t8udlWqrXHUwMDE0/EdcdTAwMDZcdTAwMTH+XHUwMDA2RZt6XHUwMDAzZEfpbyNrWFx0pVx1MDAxOMmbn9Npeey34lVcclx1MDAxMTnHqFx08IngI8BcdTAwMDDQOlBcZmKqXGKG47wnv5zM7LJC5Y3708793IVcdTAwMGaf8SV/KeZDrphVjj3IaMzk1D/a/7SCXHUwMDFiqzPzXHUwMDE32y32+1x1MDAwN/PX+1x1MDAxZqFcdTAwMTTUJytBmJg6tMBGXHUwMDEzXHUwMDBm/elcdTAwMWT1MVx1MDAxMu+PXlx1MDAxMVx1MDAxOZ+1li+ad5AgUS4uc241XHUwMDEw+aEoy1CfI7LWaTnMeraQkdX3ft9cbu9cdKmQXHUwMDE47Y36hVA/d+HDZ3zJX4j6aEY3XHUwMDBlmS9BPiuaPDWzf6FcdTAwMGZcdTAwMGZkPe2CWztbSSt4fDVTXHUwMDBlze+NXHUwMDBmPt9y8OzKmOGt74gvky+hrlx1MDAwN1x1MDAxMFxmKzHBhTDfReSsXHUwMDAxI8G0kFx1MDAwNO2TzLdcdTAwMTFo8jJEwVx1MDAwMttMvP1cdTAwMTDGKlx1MDAxNlx1MDAwN2HpzeuXh/q5K1x1MDAxZj7ja/5C1Le5pVx1MDAwNoXgrISOXHUwMDE54/Aj7i8tdPb8Nc6ZWWw7TPTJwfrXpFx1MDAxNNxncJFcdTAwMDBcdTAwMTO1VSGssqNOnyPZ6DyS+H6GYvS+iXRIYHmSuINQqUx6KsN9XHUwMDEzyVx1MDAwMWTJO9GHmFx0XG6+cV8r0oa9e3P75eF+/tKHz/iiP5P8301ik8XRrd9MgKyWXGJcdTAwMDCW+09sXHUwMDAytio37mj+XHUwMDEwVbw3t9XVXs/DzFJcdTAwMTmy2KEyYyx4YidcdTAwMDbWqeFVwvlikiPnXHUwMDE1kvUuXGIzNzKul6nMSDxcdTAwMTeB18xahcyjy9wmm8M2XHUwMDFlPXlkbcloXHUwMDE4T2KzQq1VQTH/NCWxv4trpTA/oFx1MDAxNZY5L+s5ObBcdTAwMGZu2sc7XHUwMDE1OtxuLvXh4Iw6bv78qlxmwFx1MDAwZeVcdTAwMTmRXHUwMDBmXHUwMDEwMlnK2ZF4llBLTCE4kvBcdTAwMWbQOPVTwM4rzihSkXfegTfog8TxT1x1MDAwMtsptjpk1ZSRKGgslyVmXHUwMDExRY9oPY1VRyOmUr1cdTAwMTSwc3M0lFx1MDAxZqkhO2/xOeWZ6+3ezuLa/serdn9paX55/XD3dCEthVxcXHUwMDBi5Vx1MDAxOSVGUCwhalx0k/Fxftayi1x1MDAxY1x1MDAxMYTZ8MGzXHUwMDE2otdeqDrDTHIqXHUwMDE3UnF8k2uFyLXXKs7o70VrMlwiYvuM3rBLm8yufHZcdTAwMTf9/cXPO0eLqqn6nUop6Fx1MDAxZlxcmlx1MDAwYi1VoavAg3zxMfqLOzOgKKSvoFx1MDAxOPq/SHlGZJ6XsN++1WdKRP/Xqc9I2PGdOM1brbPC6Efkb29cdTAwMWS4XHI4X/6YdFt76582afF47aJcdTAwMTTkXHUwMDBm9Vx1MDAxOVx1MDAwMFx1MDAxMK9pQY6mx42hlmV1UNZcdTAwMDZccoq51DwysCmqz1xmxLhRb9wvXHUwMDEz91+nQKNcdTAwMTBGt37jvlOhQynb7vbDLO3NTlx1MDAxYla+Jlx1MDAxYp3GcvOipeqrm76ISLaIXG6N6HphvYhcdTAwMWTlPGWIM1xiZSF0z7Ig2Fkl0ZhcdTAwMWZcdTAwMTnYVFx1MDAxNWhsaF73Q4a9MX/amf869ZnvJLEwPFWAcq+JmX+Stnoz7lr7zllz8XZleflr40qXgvlcZlx1MDAxNLElRmsl6sKRp0FcYiDy4k2dM1x1MDAwMcFcdTAwMDUx/4XqM2AlJlTZjtI37k8791+1PpNpRlx1MDAxZtP9xpHFyV3/2fx28+CiuXRSW7g5WrFmK1WXh2VIYofqjMwtgXZCXHUwMDFlm1XT4Vx1MDAwMs76iEM4XHUwMDE2zFx1MDAwM7pMPuAlXHJAWGc2XHUwMDBlQ0eo8Uz4VFx1MDAxNlvchFx1MDAwNyf6UIVWWczk0+9cdTAwMWZcdTAwMDdT4Vx1MDAxMT6P//gnXGa+XHUwMDBiaudyg1lcdTAwMTRhp1lNnsZONnt80Vvduard7KzgVvK5uf71pFxmoLagI2MkRjWOVchi01xiqDlcdTAwMDLvSVx1MDAwMo3wkIEuxquJUo68qFx1MDAxNlx1MDAxNE5ZbZR/6rlcdTAwMTkv43RcdTAwMWVE3qhQZVx1MDAxOMW0XHUwMDE32U0so/z/xjTnXHUwMDE20pWQ3lx1MDAwNFU4uVb7vOw7l/25tVx1MDAxNduq9tM67FxcptQrXHUwMDAzqokkRtYkcTA5IG38XGKoXSR2lFx1MDAwN831StuCQO1MpMREs4SCqMm6J6I0XHUwMDAxNbBccllzsOFcdTAwMTldXHUwMDFjS9BcYi3lXHUwMDFmwv83rFnnRiAyOTo0Sk5ec6h3XHUwMDBmqb6+ujm7ubjxJb2AZOOwWYrn0b01XHUwMDEy4ImdMyhcdTAwMDFg9oGvO1STKECxoUgqdMxcdTAwMTSTdbQ6XG6lTpFcdTAwMWbOSVx1MDAwNFxu8ERcdTAwMDBcIvpDXGa0Mqx00CFGjeVcdTAwMWVcdTAwMWNoNIqL6VxyLVxyqr3KzagprTWhXHUwMDAxmNxYu7U0ae23ub55vb3emYmXLs6sLVx1MDAwM6yRXHUwMDA3XHUwMDE5NfZi61x1MDAxMFx1MDAwNDAjgTWLXHUwMDFkVeFBb3FgRD/ZXHUwMDFkktv2ZDHEV4RA7J04hlx1MDAxY1x0XHUwMDEyymRcdTAwMGUwNIfwmK5GXHUwMDE5IUzpa1x1MDAxNtBQ5uHLQpuejMt9fk9cdTAwMDGKS1x1MDAwNKTJ48XGTYuPruFcdTAwMTbi7fn+rePtne5NKeJFXCJcdTAwMTWJcGYtUEFcdTAwMTHQj3v5hd2Rtlo0t1x1MDAwNO1cIkZ+LlxczG16cjZcdTAwMTKLJjdcdTAwMDFDXHUwMDAwXHUwMDA0T8HaRl5cdOhF30tYqzyOaevwXHUwMDFlXGJQ2br+XHUwMDE04VpU3K95fYjDfG3tNFr1qIz2w86HhfZsv3Nx4G6gfbhT4y/99YXFMsBa/HYkglV7NFx1MDAxYUTd0uM0XGIzRtZYgVxmia5cdTAwMTV7Xoy5fpEu1fCiXHUwMDA2I8p7XHUwMDFh7bXEXHUwMDAz+lx1MDAxNzWpapOrrjU471xi+Vx1MDAxOU2qXHUwMDFin5Ldr6tcdTAwMDe1I1+7/LK1u9vrzp03ylx1MDAwMGxcdTAwMGJcdTAwMDFT4r8pXHUwMDA0XV6N4Fx1MDAxYTFcdTAwMTL/rlx1MDAxOFx1MDAwNn2sanRcXNPUpCrL5Vx1MDAwMMD843Gd36SW26MqXHUwMDBl2TojPyZcdTAwMDb0sj7c+7RcYoszldo5XzTV0tHHq3I8Riw2L4LQryyGXHUwMDEy6HGl2jNHVrGIaqE4WZ7m/tSQozHWZ5E+PfWq51x1MDAwMjozz//oetVrNahcdTAwMWHMb1C1JDKYJvdlzZbbullaWL44OlTV08O1ai3Z2ylcdTAwMDX1gzMjXGZCTFx1MDAxOe+1XHUwMDE5eSGWZ1x1MDAxZnkySlx1MDAxNJAxLltFmr72VNBcdTAwMDDi6IpRaW/kL4T8r9SeyvlvXHUwMDBlQ/lcYlx1MDAxN54hZFx1MDAwZleXXHUwMDBm2lx1MDAxZnF99rRzrdZmb1x1MDAxYVx1MDAwYnvbqlx1MDAxNORcdTAwMGaZXHUwMDA3LzLWg6hZ0I+rXHUwMDFmnl1EXHUwMDAwXHUwMDE2Rc1qX5Dff5mXhyhwXHUwMDAzhTtcdTAwMTRccm/Mn3bmv05zqja5qXStlFxyr8pRkyt+t6bbs1x1MDAxN7O1w+Pzav+o06pvtY7PSsF8jz64fatDzV7Cw8epdLFcdTAwMDVcdTAwMTE7M3jFQMhfXHUwMDE3U1wiepnuVEPe6aLe7vrG/UK4/zrtqd9JXylcdGxcdTAwMTWhhsmrw0lcdTAwMWZcdTAwMTdnbrc2LzqL+4e3t1uzvLDYLVx1MDAwNfdZuchbXHUwMDFk8vzOXHUwMDAx4eO3XHUwMDA3XHUwMDA07pNmjy68OsCpqe5PNTq8XHUwMDA111x1MDAxN/Ncbtw38lx1MDAxN0L+1+xPXHJ52Vxc/1x1MDAwZlx1MDAwZYGyXHUwMDE1glx1MDAxZpmA21V7Nnd2Xe2qPb7Ypd3mLH5aKENcdTAwMDZbXCL6SKxcdTAwMDCFLDZmX5J6d7pccu99Z6PRiFx1MDAwMFNmmlx1MDAxYkTEaLNcdTAwMTYxw9NYcvyFpZlsoXysn896kdeIk1x1MDAwMztePejUz5bXatf987S5Nbe12a1cdTAwMWSUXHUwMDAx2OR0JFwi3lx1MDAxOPm62qJ6XHUwMDFj0TKKslx1MDAwMFx1MDAwMmNZk86Wg6evlK7C++dccmldSEz7KsB+d+9cdTAwMTI+VDqd7VQu+TA0+WpJbTu5jVx1MDAxZl3mw1VcdTAwMTJfzz058+Hz4d39+Fx1MDAwM1x1MDAxZePB9/z73d//XHUwMDAzXpv8VSJ9 + + + + + diff --git a/feed_rss_created.xml b/feed_rss_created.xml index 66d3d1cba7..0f4393f1d9 100644 --- a/feed_rss_created.xml +++ b/feed_rss_created.xml @@ -1 +1 @@ - Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 03 Oct 2024 10:06:32 -0000 Thu, 03 Oct 2024 10:06:32 -0000 1440 MkDocs RSS plugin - v1.15.0 Anatomy of a Textual User Interface willmcgugan DevLog <h1>Anatomy of a Textual User Interface</h1><p>!!! note "My bad 🤦"</p><pre><code>The date is wrong on this post&amp;mdash;it was actually published on the 2nd of September 2024.I don't want to fix it, as that would break the URL.</code></pre><p>I recently wrote a <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> to chat to an AI agent in the terminal.I'm not the first to do this (shout out to <a href="https://github.com/darrenburns/elia">Elia</a> and <a href="https://github.com/villekr/paita">Paita</a>), but I <em>may</em> be the first to have it reply as if it were the AI from the Aliens movies?</p><p>Here's a video of it in action:</p><iframe width="100%" style="aspect-ratio:1512 / 982" src="https://www.youtube.com/embed/hr5JvQS4d_w" title="Mother AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>Now let's dissect the code like Bishop dissects a facehugger.</p>https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Sun, 15 Sep 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Towards Textual Web Applications darrenburns DevLog <p>In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.</p>https://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Sun, 08 Sep 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Behind the Curtain of Inline Terminal Applications willmcgugan DevLog <h1>Behind the Curtain of Inline Terminal Applications</h1><p>Textual recently added the ability to run <em>inline</em> terminal apps.You can see this in action if you run the <a href="https://github.com/Textualize/textual/blob/main/examples/calculator.py">calculator example</a>:</p><p><img alt="Inline Calculator" src="../images/calcinline.png"></p><p>The application appears directly under the prompt, rather than occupying the full height of the screen&mdash;which is more typical of TUI applications.You can interact with this calculator using keys <em>or</em> the mouse.When you press ++ctrl+c++ the calculator disappears and returns you to the prompt.</p><p>Here's another app that creates an inline code editor:</p><p>=== "Video"</p><pre><code>&lt;div class="video-wrapper"&gt; &lt;iframe width="852" height="525" src="https://www.youtube.com/embed/Dt70oSID1DY" title="Inline app" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;</code></pre><p>=== "inline.py" ```python from textual.app import App, ComposeResult from textual.widgets import TextArea</p><pre><code>class InlineApp(App): CSS = """ TextArea { height: auto; max-height: 50vh; } """ def compose(self) -&gt; ComposeResult: yield TextArea(language="python")if __name__ == "__main__": InlineApp().run(inline=True)```</code></pre><p>This post will cover some of what goes on under the hood to make such inline apps work.</p><p>It's not going to go in to too much detail.I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.</p>https://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Sat, 20 Apr 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Remote memory profiling with Memray willmcgugan DevLog <h1>Remote memory profiling with Memray</h1><p><a href="https://github.com/bloomberg/memray">Memray</a> is a memory profiler for Python, built by some very smart devs at Bloomberg.It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!</p><p>They recently added a <a href="https://github.com/textualize/textual/">Textual</a> interface which looks amazing, and lets you monitor your process right from the terminal:</p><p><img alt="Memray" src="https://raw.githubusercontent.com/bloomberg/memray/main/docs/_static/images/live_animated.webp"></p>https://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Tue, 20 Feb 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ File magic with the Python standard library willmcgugan DevLog <h1>File magic with the Python standard library</h1><p>I recently published <a href="https://github.com/textualize/toolong">Toolong</a>, an app for viewing log files.There were some interesting technical challenges in building Toolong that I'd like to cover in this post.</p>https://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Sun, 11 Feb 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Announcing textual-plotext davep DevLog <h1>Announcing textual-plotext</h1><p>It's no surprise that a common question on the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discordserver</a> is how to go about producing plots inthe terminal. A popular solution that has been suggested is<a href="https://github.com/piccolomo/plotext">Plotext</a>. While Plotext doesn'tdirectly support Textual, it is <a href="https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich">easy to use withRich</a>and, because of this, we wanted to make it just as easy to use in yourTextual applications.</p>https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Wed, 04 Oct 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned building a text editor for the terminal</h1><p><code>TextArea</code> is the latest widget to be added to Textual's <a href="https://textual.textualize.io/widget_gallery/">growing collection</a>.It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.</p><p><img alt="text-area-welcome.gif" src="../images/text-area-learnings/text-area-welcome.gif"></p><p>Adding a <code>TextArea</code> to your Textual app is as simple as adding this to your <code>compose</code> method:</p><p><code>pythonyield TextArea()</code></p><p>Enabling syntax highlighting for a language is as simple as:</p><p><code>pythonyield TextArea(language="python")</code></p><p>Working on the <code>TextArea</code> widget for Textual taught me a lot about Python and my generalapproach to software engineering. It gave me an appreciation for the subtle functionality behindthe editors we use on a daily basis — features we may not even notice, despitesome engineer spending hours perfecting it to provide a small boost to our development experience.</p><p>This post is a tour of some of these learnings.</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Mon, 18 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and <a href="https://github.com/Textualize/textual">Textual</a> &mdash; two popular Python libraries that work magic in the terminal.</p><p>!!! note</p><pre><code>Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth)</code></pre><p>Today we are adding one project more to that lineup: <a href="https://github.com/Textualize/textual-web">textual-web</a>.</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is <code>inspect</code> which is so useful you may want to <code>pip install rich</code> just for this feature.</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>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.</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it has been around forever.</p><p>Terminals are one of those forever-technologies.</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Tue, 06 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.</p><p>We've been a little distracted with our "dogfood" projects: <a href="https://github.com/Textualize/frogmouth">Frogmouth</a> and <a href="https://github.com/Textualize/trogon">Trogon</a>. 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.</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>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.</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Mon, 08 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with <a href="https://github.com/Textualize/textual">Textual</a>, as part of our <em>dog-fooding</em> week.The first app, <a href="https://github.com/Textualize/frogmouth">Frogmouth</a>, was released at the weekend and already has 1K GitHub stars!Expect two more such apps this month.</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>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).</p><p>What's new in this release?</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ \ No newline at end of file + Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 10 Oct 2024 13:55:22 -0000 Thu, 10 Oct 2024 13:55:22 -0000 1440 MkDocs RSS plugin - v1.15.0 Anatomy of a Textual User Interface willmcgugan DevLog <h1>Anatomy of a Textual User Interface</h1><p>!!! note "My bad 🤦"</p><pre><code>The date is wrong on this post&amp;mdash;it was actually published on the 2nd of September 2024.I don't want to fix it, as that would break the URL.</code></pre><p>I recently wrote a <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> to chat to an AI agent in the terminal.I'm not the first to do this (shout out to <a href="https://github.com/darrenburns/elia">Elia</a> and <a href="https://github.com/villekr/paita">Paita</a>), but I <em>may</em> be the first to have it reply as if it were the AI from the Aliens movies?</p><p>Here's a video of it in action:</p><iframe width="100%" style="aspect-ratio:1512 / 982" src="https://www.youtube.com/embed/hr5JvQS4d_w" title="Mother AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>Now let's dissect the code like Bishop dissects a facehugger.</p>https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Sun, 15 Sep 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Towards Textual Web Applications darrenburns DevLog <p>In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.</p>https://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Sun, 08 Sep 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Behind the Curtain of Inline Terminal Applications willmcgugan DevLog <h1>Behind the Curtain of Inline Terminal Applications</h1><p>Textual recently added the ability to run <em>inline</em> terminal apps.You can see this in action if you run the <a href="https://github.com/Textualize/textual/blob/main/examples/calculator.py">calculator example</a>:</p><p><img alt="Inline Calculator" src="../images/calcinline.png"></p><p>The application appears directly under the prompt, rather than occupying the full height of the screen&mdash;which is more typical of TUI applications.You can interact with this calculator using keys <em>or</em> the mouse.When you press ++ctrl+c++ the calculator disappears and returns you to the prompt.</p><p>Here's another app that creates an inline code editor:</p><p>=== "Video"</p><pre><code>&lt;div class="video-wrapper"&gt; &lt;iframe width="852" height="525" src="https://www.youtube.com/embed/Dt70oSID1DY" title="Inline app" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;</code></pre><p>=== "inline.py" ```python from textual.app import App, ComposeResult from textual.widgets import TextArea</p><pre><code>class InlineApp(App): CSS = """ TextArea { height: auto; max-height: 50vh; } """ def compose(self) -&gt; ComposeResult: yield TextArea(language="python")if __name__ == "__main__": InlineApp().run(inline=True)```</code></pre><p>This post will cover some of what goes on under the hood to make such inline apps work.</p><p>It's not going to go in to too much detail.I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.</p>https://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Sat, 20 Apr 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Remote memory profiling with Memray willmcgugan DevLog <h1>Remote memory profiling with Memray</h1><p><a href="https://github.com/bloomberg/memray">Memray</a> is a memory profiler for Python, built by some very smart devs at Bloomberg.It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!</p><p>They recently added a <a href="https://github.com/textualize/textual/">Textual</a> interface which looks amazing, and lets you monitor your process right from the terminal:</p><p><img alt="Memray" src="https://raw.githubusercontent.com/bloomberg/memray/main/docs/_static/images/live_animated.webp"></p>https://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Tue, 20 Feb 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ File magic with the Python standard library willmcgugan DevLog <h1>File magic with the Python standard library</h1><p>I recently published <a href="https://github.com/textualize/toolong">Toolong</a>, an app for viewing log files.There were some interesting technical challenges in building Toolong that I'd like to cover in this post.</p>https://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Sun, 11 Feb 2024 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Announcing textual-plotext davep DevLog <h1>Announcing textual-plotext</h1><p>It's no surprise that a common question on the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discordserver</a> is how to go about producing plots inthe terminal. A popular solution that has been suggested is<a href="https://github.com/piccolomo/plotext">Plotext</a>. While Plotext doesn'tdirectly support Textual, it is <a href="https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich">easy to use withRich</a>and, because of this, we wanted to make it just as easy to use in yourTextual applications.</p>https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Wed, 04 Oct 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned building a text editor for the terminal</h1><p><code>TextArea</code> is the latest widget to be added to Textual's <a href="https://textual.textualize.io/widget_gallery/">growing collection</a>.It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.</p><p><img alt="text-area-welcome.gif" src="../images/text-area-learnings/text-area-welcome.gif"></p><p>Adding a <code>TextArea</code> to your Textual app is as simple as adding this to your <code>compose</code> method:</p><p><code>pythonyield TextArea()</code></p><p>Enabling syntax highlighting for a language is as simple as:</p><p><code>pythonyield TextArea(language="python")</code></p><p>Working on the <code>TextArea</code> widget for Textual taught me a lot about Python and my generalapproach to software engineering. It gave me an appreciation for the subtle functionality behindthe editors we use on a daily basis — features we may not even notice, despitesome engineer spending hours perfecting it to provide a small boost to our development experience.</p><p>This post is a tour of some of these learnings.</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Mon, 18 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and <a href="https://github.com/Textualize/textual">Textual</a> &mdash; two popular Python libraries that work magic in the terminal.</p><p>!!! note</p><pre><code>Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth)</code></pre><p>Today we are adding one project more to that lineup: <a href="https://github.com/Textualize/textual-web">textual-web</a>.</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is <code>inspect</code> which is so useful you may want to <code>pip install rich</code> just for this feature.</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>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.</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it has been around forever.</p><p>Terminals are one of those forever-technologies.</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Tue, 06 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.</p><p>We've been a little distracted with our "dogfood" projects: <a href="https://github.com/Textualize/frogmouth">Frogmouth</a> and <a href="https://github.com/Textualize/trogon">Trogon</a>. 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.</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>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.</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Mon, 08 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with <a href="https://github.com/Textualize/textual">Textual</a>, as part of our <em>dog-fooding</em> week.The first app, <a href="https://github.com/Textualize/frogmouth">Frogmouth</a>, was released at the weekend and already has 1K GitHub stars!Expect two more such apps this month.</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>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).</p><p>What's new in this release?</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ \ No newline at end of file diff --git a/feed_rss_updated.xml b/feed_rss_updated.xml index a0362467a2..2881084968 100644 --- a/feed_rss_updated.xml +++ b/feed_rss_updated.xml @@ -1 +1 @@ - Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 03 Oct 2024 10:06:32 -0000 Thu, 03 Oct 2024 10:06:32 -0000 1440 MkDocs RSS plugin - v1.15.0 Anatomy of a Textual User Interface willmcgugan DevLog <h1>Anatomy of a Textual User Interface</h1><p>!!! note "My bad 🤦"</p><pre><code>The date is wrong on this post&amp;mdash;it was actually published on the 2nd of September 2024.I don't want to fix it, as that would break the URL.</code></pre><p>I recently wrote a <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> to chat to an AI agent in the terminal.I'm not the first to do this (shout out to <a href="https://github.com/darrenburns/elia">Elia</a> and <a href="https://github.com/villekr/paita">Paita</a>), but I <em>may</em> be the first to have it reply as if it were the AI from the Aliens movies?</p><p>Here's a video of it in action:</p><iframe width="100%" style="aspect-ratio:1512 / 982" src="https://www.youtube.com/embed/hr5JvQS4d_w" title="Mother AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>Now let's dissect the code like Bishop dissects a facehugger.</p>https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Sun, 08 Sep 2024 12:17:42 +0000Textualhttps://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Towards Textual Web Applications darrenburns DevLog <p>In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.</p>https://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Sun, 08 Sep 2024 12:17:42 +0000Textualhttps://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Behind the Curtain of Inline Terminal Applications willmcgugan DevLog <h1>Behind the Curtain of Inline Terminal Applications</h1><p>Textual recently added the ability to run <em>inline</em> terminal apps.You can see this in action if you run the <a href="https://github.com/Textualize/textual/blob/main/examples/calculator.py">calculator example</a>:</p><p><img alt="Inline Calculator" src="../images/calcinline.png"></p><p>The application appears directly under the prompt, rather than occupying the full height of the screen&mdash;which is more typical of TUI applications.You can interact with this calculator using keys <em>or</em> the mouse.When you press ++ctrl+c++ the calculator disappears and returns you to the prompt.</p><p>Here's another app that creates an inline code editor:</p><p>=== "Video"</p><pre><code>&lt;div class="video-wrapper"&gt; &lt;iframe width="852" height="525" src="https://www.youtube.com/embed/Dt70oSID1DY" title="Inline app" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;</code></pre><p>=== "inline.py" ```python from textual.app import App, ComposeResult from textual.widgets import TextArea</p><pre><code>class InlineApp(App): CSS = """ TextArea { height: auto; max-height: 50vh; } """ def compose(self) -&gt; ComposeResult: yield TextArea(language="python")if __name__ == "__main__": InlineApp().run(inline=True)```</code></pre><p>This post will cover some of what goes on under the hood to make such inline apps work.</p><p>It's not going to go in to too much detail.I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.</p>https://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Sun, 21 Apr 2024 13:21:53 +0000Textualhttps://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ File magic with the Python standard library willmcgugan DevLog <h1>File magic with the Python standard library</h1><p>I recently published <a href="https://github.com/textualize/toolong">Toolong</a>, an app for viewing log files.There were some interesting technical challenges in building Toolong that I'd like to cover in this post.</p>https://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Sun, 03 Mar 2024 13:32:04 +0000Textualhttps://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Remote memory profiling with Memray willmcgugan DevLog <h1>Remote memory profiling with Memray</h1><p><a href="https://github.com/bloomberg/memray">Memray</a> is a memory profiler for Python, built by some very smart devs at Bloomberg.It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!</p><p>They recently added a <a href="https://github.com/textualize/textual/">Textual</a> interface which looks amazing, and lets you monitor your process right from the terminal:</p><p><img alt="Memray" src="https://raw.githubusercontent.com/bloomberg/memray/main/docs/_static/images/live_animated.webp"></p>https://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Tue, 20 Feb 2024 15:54:07 +0000Textualhttps://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Announcing textual-plotext davep DevLog <h1>Announcing textual-plotext</h1><p>It's no surprise that a common question on the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discordserver</a> is how to go about producing plots inthe terminal. A popular solution that has been suggested is<a href="https://github.com/piccolomo/plotext">Plotext</a>. While Plotext doesn'tdirectly support Textual, it is <a href="https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich">easy to use withRich</a>and, because of this, we wanted to make it just as easy to use in yourTextual applications.</p>https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Sat, 07 Oct 2023 13:42:11 +0000Textualhttps://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned building a text editor for the terminal</h1><p><code>TextArea</code> is the latest widget to be added to Textual's <a href="https://textual.textualize.io/widget_gallery/">growing collection</a>.It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.</p><p><img alt="text-area-welcome.gif" src="../images/text-area-learnings/text-area-welcome.gif"></p><p>Adding a <code>TextArea</code> to your Textual app is as simple as adding this to your <code>compose</code> method:</p><p><code>pythonyield TextArea()</code></p><p>Enabling syntax highlighting for a language is as simple as:</p><p><code>pythonyield TextArea(language="python")</code></p><p>Working on the <code>TextArea</code> widget for Textual taught me a lot about Python and my generalapproach to software engineering. It gave me an appreciation for the subtle functionality behindthe editors we use on a daily basis — features we may not even notice, despitesome engineer spending hours perfecting it to provide a small boost to our development experience.</p><p>This post is a tour of some of these learnings.</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Sat, 23 Sep 2023 14:06:20 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 13:27:43 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 17:01:09 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and <a href="https://github.com/Textualize/textual">Textual</a> &mdash; two popular Python libraries that work magic in the terminal.</p><p>!!! note</p><pre><code>Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth)</code></pre><p>Today we are adding one project more to that lineup: <a href="https://github.com/Textualize/textual-web">textual-web</a>.</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 17:53:31 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 17:05:04 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is <code>inspect</code> which is so useful you may want to <code>pip install rich</code> just for this feature.</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 12:34:46 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 14:08:32 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>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.</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 16:09:24 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it has been around forever.</p><p>Terminals are one of those forever-technologies.</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Mon, 05 Jun 2023 17:51:19 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.</p><p>We've been a little distracted with our "dogfood" projects: <a href="https://github.com/Textualize/frogmouth">Frogmouth</a> and <a href="https://github.com/Textualize/trogon">Trogon</a>. 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.</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 17:41:08 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>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.</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Thu, 01 Jun 2023 11:33:54 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with <a href="https://github.com/Textualize/textual">Textual</a>, as part of our <em>dog-fooding</em> week.The first app, <a href="https://github.com/Textualize/frogmouth">Frogmouth</a>, was released at the weekend and already has 1K GitHub stars!Expect two more such apps this month.</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 13:22:22 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.11.0 adds a beautiful Markdown widget willmcgugan Release <h1>Textual 0.11.0 adds a beautiful Markdown widget</h1><p>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?</p>https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Sat, 08 Apr 2023 15:35:49 +0000Textualhttps://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 13:12:51 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ \ No newline at end of file + Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 10 Oct 2024 13:55:22 -0000 Thu, 10 Oct 2024 13:55:22 -0000 1440 MkDocs RSS plugin - v1.15.0 Anatomy of a Textual User Interface willmcgugan DevLog <h1>Anatomy of a Textual User Interface</h1><p>!!! note "My bad 🤦"</p><pre><code>The date is wrong on this post&amp;mdash;it was actually published on the 2nd of September 2024.I don't want to fix it, as that would break the URL.</code></pre><p>I recently wrote a <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> to chat to an AI agent in the terminal.I'm not the first to do this (shout out to <a href="https://github.com/darrenburns/elia">Elia</a> and <a href="https://github.com/villekr/paita">Paita</a>), but I <em>may</em> be the first to have it reply as if it were the AI from the Aliens movies?</p><p>Here's a video of it in action:</p><iframe width="100%" style="aspect-ratio:1512 / 982" src="https://www.youtube.com/embed/hr5JvQS4d_w" title="Mother AI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>Now let's dissect the code like Bishop dissects a facehugger.</p>https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Sun, 08 Sep 2024 12:17:42 +0000Textualhttps://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ Towards Textual Web Applications darrenburns DevLog <p>In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.</p>https://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Sun, 08 Sep 2024 12:17:42 +0000Textualhttps://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ Behind the Curtain of Inline Terminal Applications willmcgugan DevLog <h1>Behind the Curtain of Inline Terminal Applications</h1><p>Textual recently added the ability to run <em>inline</em> terminal apps.You can see this in action if you run the <a href="https://github.com/Textualize/textual/blob/main/examples/calculator.py">calculator example</a>:</p><p><img alt="Inline Calculator" src="../images/calcinline.png"></p><p>The application appears directly under the prompt, rather than occupying the full height of the screen&mdash;which is more typical of TUI applications.You can interact with this calculator using keys <em>or</em> the mouse.When you press ++ctrl+c++ the calculator disappears and returns you to the prompt.</p><p>Here's another app that creates an inline code editor:</p><p>=== "Video"</p><pre><code>&lt;div class="video-wrapper"&gt; &lt;iframe width="852" height="525" src="https://www.youtube.com/embed/Dt70oSID1DY" title="Inline app" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;</code></pre><p>=== "inline.py" ```python from textual.app import App, ComposeResult from textual.widgets import TextArea</p><pre><code>class InlineApp(App): CSS = """ TextArea { height: auto; max-height: 50vh; } """ def compose(self) -&gt; ComposeResult: yield TextArea(language="python")if __name__ == "__main__": InlineApp().run(inline=True)```</code></pre><p>This post will cover some of what goes on under the hood to make such inline apps work.</p><p>It's not going to go in to too much detail.I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.</p>https://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ Sun, 21 Apr 2024 13:21:53 +0000Textualhttps://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ File magic with the Python standard library willmcgugan DevLog <h1>File magic with the Python standard library</h1><p>I recently published <a href="https://github.com/textualize/toolong">Toolong</a>, an app for viewing log files.There were some interesting technical challenges in building Toolong that I'd like to cover in this post.</p>https://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Sun, 03 Mar 2024 13:32:04 +0000Textualhttps://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ Remote memory profiling with Memray willmcgugan DevLog <h1>Remote memory profiling with Memray</h1><p><a href="https://github.com/bloomberg/memray">Memray</a> is a memory profiler for Python, built by some very smart devs at Bloomberg.It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!</p><p>They recently added a <a href="https://github.com/textualize/textual/">Textual</a> interface which looks amazing, and lets you monitor your process right from the terminal:</p><p><img alt="Memray" src="https://raw.githubusercontent.com/bloomberg/memray/main/docs/_static/images/live_animated.webp"></p>https://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Tue, 20 Feb 2024 15:54:07 +0000Textualhttps://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ Announcing textual-plotext davep DevLog <h1>Announcing textual-plotext</h1><p>It's no surprise that a common question on the <a href="https://discord.gg/Enf6Z3qhVr">Textual Discordserver</a> is how to go about producing plots inthe terminal. A popular solution that has been suggested is<a href="https://github.com/piccolomo/plotext">Plotext</a>. While Plotext doesn'tdirectly support Textual, it is <a href="https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich">easy to use withRich</a>and, because of this, we wanted to make it just as easy to use in yourTextual applications.</p>https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Sat, 07 Oct 2023 13:42:11 +0000Textualhttps://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned building a text editor for the terminal</h1><p><code>TextArea</code> is the latest widget to be added to Textual's <a href="https://textual.textualize.io/widget_gallery/">growing collection</a>.It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.</p><p><img alt="text-area-welcome.gif" src="../images/text-area-learnings/text-area-welcome.gif"></p><p>Adding a <code>TextArea</code> to your Textual app is as simple as adding this to your <code>compose</code> method:</p><p><code>pythonyield TextArea()</code></p><p>Enabling syntax highlighting for a language is as simple as:</p><p><code>pythonyield TextArea(language="python")</code></p><p>Working on the <code>TextArea</code> widget for Textual taught me a lot about Python and my generalapproach to software engineering. It gave me an appreciation for the subtle functionality behindthe editors we use on a daily basis — features we may not even notice, despitesome engineer spending hours perfecting it to provide a small boost to our development experience.</p><p>This post is a tour of some of these learnings.</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Sat, 23 Sep 2023 14:06:20 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 13:27:43 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 17:01:09 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and <a href="https://github.com/Textualize/textual">Textual</a> &mdash; two popular Python libraries that work magic in the terminal.</p><p>!!! note</p><pre><code>Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth)</code></pre><p>Today we are adding one project more to that lineup: <a href="https://github.com/Textualize/textual-web">textual-web</a>.</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 17:53:31 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 17:05:04 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is <code>inspect</code> which is so useful you may want to <code>pip install rich</code> just for this feature.</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 12:34:46 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 14:08:32 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>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.</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 16:09:24 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it has been around forever.</p><p>Terminals are one of those forever-technologies.</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Mon, 05 Jun 2023 17:51:19 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.</p><p>We've been a little distracted with our "dogfood" projects: <a href="https://github.com/Textualize/frogmouth">Frogmouth</a> and <a href="https://github.com/Textualize/trogon">Trogon</a>. 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.</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 17:41:08 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>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.</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Thu, 01 Jun 2023 11:33:54 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with <a href="https://github.com/Textualize/textual">Textual</a>, as part of our <em>dog-fooding</em> week.The first app, <a href="https://github.com/Textualize/frogmouth">Frogmouth</a>, was released at the weekend and already has 1K GitHub stars!Expect two more such apps this month.</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 13:22:22 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.11.0 adds a beautiful Markdown widget willmcgugan Release <h1>Textual 0.11.0 adds a beautiful Markdown widget</h1><p>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?</p>https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Sat, 08 Apr 2023 15:35:49 +0000Textualhttps://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 13:12:51 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ \ No newline at end of file diff --git a/guide/reactivity/index.html b/guide/reactivity/index.html index 3ad97468ca..b9c5a5c18b 100644 --- a/guide/reactivity/index.html +++ b/guide/reactivity/index.html @@ -8152,132 +8152,131 @@

    Recompose + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Clock + Clock - + - - - - - - - - - - - - ┓  ┓    ┏━┓╺━┓   ╻ ╻┏━╸ - ┃  ┃  : ┃ ┃  ┃ : ┗━┫┗━┓ -╺┻╸╺┻╸   ┗━┛  ╹     ╹╺━┛ - - - - - - - - - - + + + + + + + + + + + +╶╮ ╷ ╷   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─┤ : ╰─╮├─╮ :  ─┤╰─╮ +╶┴╴  ╵   ╶─╯╰─╯   ╶─╯╶─╯ + + + + + + + + + + @@ -8345,132 +8344,131 @@

    Recompose + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Clock + Clock - + - - - - - - - - - - - - ┓  ┓    ┏━┓╺━┓   ╻ ╻┏━╸ - ┃  ┃  : ┃ ┃  ┃ : ┗━┫┗━┓ -╺┻╸╺┻╸   ┗━┛  ╹     ╹╺━┛ - - - - - - - - - - + + + + + + + + + + + +╶╮ ╷ ╷   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─┤ : ╰─╮├─╮ :  ─┤╰─╮ +╶┴╴  ╵   ╶─╯╰─╯   ╶─╯╶─╯ + + + + + + + + + + @@ -9348,136 +9346,136 @@

    Data binding - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WorldClockApp + WorldClockApp - + - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/London - ┓  ┓    ┏━┓╺━┓   ╻ ╻┏━╸ - ┃  ┃  : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺┻╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/Paris - ┓ ╺━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┏━┛ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸┗━╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Asia/Tokyo - ┓ ┏━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┗━┫ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺━┛   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/London +╶╮ ╷ ╷   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─┤ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴  ╵   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/Paris +╶╮ ╭─╴   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─╮ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴╶─╯   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Asia/Tokyo +╶─╮╶─╮   ╭─╴╭─╴   ╶─╮╭─╴ +┌─┘┌─┘ : ╰─╮├─╮ :  ─┤├─╮ +╰─╴╰─╴   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ @@ -9586,136 +9584,136 @@

    Data binding - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WorldClockApp + WorldClockApp - + - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/London - ┓  ┓    ┏━┓╺━┓   ╻ ╻┏━╸ - ┃  ┃  : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺┻╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/Paris - ┓ ╺━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┏━┛ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸┗━╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Asia/Tokyo - ┓ ┏━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┗━┫ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺━┛   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/London +╶╮ ╷ ╷   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─┤ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴  ╵   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/Paris +╶╮ ╭─╴   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─╮ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴╶─╯   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Asia/Tokyo +╶─╮╶─╮   ╭─╴╭─╴   ╶─╮╭─╴ +┌─┘┌─┘ : ╰─╮├─╮ :  ─┤├─╮ +╰─╴╰─╴   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ @@ -9832,136 +9830,136 @@

    Data binding - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WorldClockApp + WorldClockApp - + - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/London - ┓  ┓    ┏━┓╺━┓   ╻ ╻┏━╸ - ┃  ┃  : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺┻╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Europe/Paris - ┓ ╺━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┏━┛ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸┗━╸   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -Asia/Tokyo - ┓ ┏━┓   ┏━┓╺━┓   ╻ ╻┏━╸ - ┃ ┗━┫ : ┃ ┃  ┃ : ┗━┫┣━┓ -╺┻╸╺━┛   ┗━┛  ╹     ╹┗━┛ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/London +╶╮ ╷ ╷   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─┤ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴  ╵   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Europe/Paris +╶╮ ╭─╴   ╭─╴╭─╴   ╶─╮╭─╴ + │ ╰─╮ : ╰─╮├─╮ :  ─┤├─╮ +╶┴╴╶─╯   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Asia/Tokyo +╶─╮╶─╮   ╭─╴╭─╴   ╶─╮╭─╴ +┌─┘┌─┘ : ╰─╮├─╮ :  ─┤├─╮ +╰─╴╰─╴   ╶─╯╰─╯   ╶─╯╰─╯ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ diff --git a/guide/screens/index.html b/guide/screens/index.html index 1dca27ef64..52f16644b8 100644 --- a/guide/screens/index.html +++ b/guide/screens/index.html @@ -6845,15 +6845,15 @@

    Creating a screen""" -class BSOD(Screen): - BINDINGS = [("escape", "app.pop_screen", "Pop screen")] +class BSOD(Screen): + BINDINGS = [("escape", "app.pop_screen", "Pop screen")] def compose(self) -> ComposeResult: yield Static(" Windows ", id="title") yield Static(ERROR_TEXT) yield Static("Press any key to continue [blink]_[/]", id="any-key") - - + + class BSODApp(App): CSS_PATH = "screen01.tcss" SCREENS = {"bsod": BSOD} @@ -7082,8 +7082,8 @@

    Named screens def on_mount(self) -> None: self.install_screen(BSOD(), name="bsod") - - + + if __name__ == "__main__": app = BSODApp() app.run() @@ -9022,7 +9022,7 @@

    Screen events - June 6, 2024 + October 3, 2024 diff --git a/guide/testing/index.html b/guide/testing/index.html index 0d3575dc63..71a51ac908 100644 --- a/guide/testing/index.html +++ b/guide/testing/index.html @@ -7164,210 +7164,210 @@

    Creating a snapshot test - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CalculatorApp + CalculatorApp - + - - - - - -╺━┓  ┓ ╻ ╻┏━╸┏━┓╺━┓ - ━┫  ┃ ┗━┫┗━┓┗━┫┏━┛ -╺━┛.╺┻╸  ╹╺━┛╺━┛┗━╸ - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -C+/-%÷ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -789× - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -456- - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -123+ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -0.= - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +╶─╮ ╶╮ ╷ ╷╭─╴╭─╮╶─╮ + ─┤  │ ╰─┤╰─╮╰─┤┌─┘ +╶─╯.╶┴╴  ╵╶─╯╶─╯╰─╴ + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +C+/-%÷ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +789× + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +456- + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +123+ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +0.= + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/guide/widgets/index.html b/guide/widgets/index.html index 159f07556f..c05a9bc4cd 100644 --- a/guide/widgets/index.html +++ b/guide/widgets/index.html @@ -9424,134 +9424,133 @@

    Tooltips + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TooltipApp + TooltipApp - - - - - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁I must not fear. -Fear is the mind-killer. -Fear is the little-death that brings -total obliteration. -I will face my fear. - - - - - - + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings +total obliteration. +I will face my fear. + + + + + + @@ -9777,134 +9776,133 @@

    Customizing the tooltip - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TooltipApp + TooltipApp - - - - - - - - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁ -I must not fear. -Fear is the mind-killer. -Fear is the little-death that  -brings total obliteration. -I will face my fear. - - - - - + + + + + + + + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + +I must not fear. +Fear is the mind-killer. +Fear is the little-death that  +brings total obliteration. +I will face my fear. + + + + + @@ -10003,136 +10001,136 @@

    Loading indicator + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DataApp + DataApp - + - - - - - - -● ● ● ● ● ● ● ●  - - - - - - - - - - - -● ● ● ● ● ● ● ●  - - - - - + + + + + + +● ● ● ● ● ● ● ●  + + + + + + + + + + + +● ● ● ● ● ● ● ●  + + + + + diff --git a/how-to/package-with-hatch/index.html b/how-to/package-with-hatch/index.html index 0696e4fad9..d530cf0360 100644 --- a/how-to/package-with-hatch/index.html +++ b/how-to/package-with-hatch/index.html @@ -6827,210 +6827,210 @@

    Package a Textual app with Hatch - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CalculatorApp + CalculatorApp - + - - - - - -╺━┓  ┓ ╻ ╻┏━╸┏━┓╺━┓ - ━┫  ┃ ┗━┫┗━┓┗━┫┏━┛ -╺━┛.╺┻╸  ╹╺━┛╺━┛┗━╸ - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -C+/-%÷ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -789× - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -456- - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -123+ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -0.= - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +╶─╮ ╶╮ ╷ ╷╭─╴╭─╮╶─╮ + ─┤  │ ╰─┤╰─╮╰─┤┌─┘ +╶─╯.╶┴╴  ╵╶─╯╶─╯╰─╴ + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +C+/-%÷ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +789× + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +456- + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +123+ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +0.= + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/how-to/render-and-compose/index.html b/how-to/render-and-compose/index.html index 63f40e6ee8..d141a09e73 100644 --- a/how-to/render-and-compose/index.html +++ b/how-to/render-and-compose/index.html @@ -6566,978 +6566,972 @@

    Combining render and compose + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SplashApp + SplashApp - - - - - - - - - - - - - - - - - - - - - - -Making a splash with Textual! - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +Making a splash with Textual! + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html index 8e1442e11a..47924e306c 100644 --- a/index.html +++ b/index.html @@ -7542,210 +7542,210 @@

    What is Textual? + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CalculatorApp + CalculatorApp - + - - - - - -╺━┓  ┓ ╻ ╻┏━╸┏━┓╺━┓ - ━┫  ┃ ┗━┫┗━┓┗━┫┏━┛ -╺━┛.╺┻╸  ╹╺━┛╺━┛┗━╸ - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -C+/-%÷ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -789× - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -456- - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -123+ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -0.= - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +╶─╮ ╶╮ ╷ ╷╭─╴╭─╮╶─╮ + ─┤  │ ╰─┤╰─╮╰─┤┌─┘ +╶─╯.╶┴╴  ╵╶─╯╶─╯╰─╴ + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +C+/-%÷ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +789× + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +456- + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +123+ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +0.= + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/objects.inv b/objects.inv index b28bd80bfa..186ccb6cb6 100644 Binary files a/objects.inv and b/objects.inv differ diff --git a/search/search_index.json b/search/search_index.json index b9a5dfeecb..80ed63a18d 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.

    UI \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\u258e\u258a\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\u258e \u258afeeds\u258e\u258a\u2590X\u258c\u00a0Case\u00a0sensitive\u258e\u258a\u2590X\u258c\u00a0Regex\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\u258e\u258a\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503162.71.236.120\u00a0-\u00a0-\u00a0[29/Jan/2024:13:34:58\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Net\u2503 \u250352.70.240.171\u00a0-\u00a0-\u00a0[29/Jan/2024:13:35:33\u00a0+0000]\"GET\u00a0/2007/07/10/postmarkup-105/\u00a0HTTP/1.1\"3010\u2503 \u2503121.137.55.45\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:19\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u250398.207.26.211\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:37\u00a0+0000]\"GET\u00a0/feeds/posts\u00a0HTTP/1.1\"3070\"-\"\"Mozilla/5.\u2503 \u250398.207.26.211\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:42\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"20098063\"-\"\"Mozil\u2503 \u250318.183.222.19\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:44\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u250366.249.64.164\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:46\u00a0+0000]\"GET\u00a0/blog/tech/post/a-texture-mapped-spinning-3d\u2503 \u2503116.203.207.165\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:55\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"2001182\u2503 \u2503128.65.195.158\u00a0-\u00a0-\u00a0[29/Jan/2024:13:38:44\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"https:/\u2503 \u2503128.65.195.158\u00a0-\u00a0-\u00a0[29/Jan/2024:13:38:46\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"https:/\u2503 \u250351.222.253.12\u00a0-\u00a0-\u00a0[29/Jan/2024:13:41:17\u00a0+0000]\"GET\u00a0/blog/tech/post/css-in-the-terminal-with-pyt\u2503 \u2503154.159.237.77\u00a0-\u00a0-\u00a0[29/Jan/2024:13:42:28\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Moz\u2503 \u250392.247.181.10\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:23\u00a0+0000]\"GET\u00a0/feed/\u00a0HTTP/1.1\"200107059\"https://www.wil\u2503 \u2503134.209.40.52\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:41\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"200118238\u2503 \u2503192.3.134.205\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:55\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Mozi\u2503 \u2503174.136.108.22\u00a0-\u00a0-\u00a0[29/Jan/2024:13:44:42\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Tin\u2503 \u250364.71.157.117\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:16\u00a0+0000]\"GET\u00a0/feed/\u00a0HTTP/1.1\"200107059\"-\"\"Feedbin\u00a0fee\u2503 \u2503121.137.55.45\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:19\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u2503216.244.66.233\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:22\u00a0+0000]\"GET\u00a0/robots.txt\u00a0HTTP/1.1\"200132\"-\"\"Mozilla/\u2503 \u250378.82.5.250\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:29\u00a0+0000]\"GET\u00a0/blog/tech/post/real-working-hyperlinks-in-the\u2503 \u250378.82.5.250\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:30\u00a0+0000]\"GET\u00a0/favicon.ico\u00a0HTTP/1.1\"2005694\"https://www.w\u2581\u2581\u2503 \u250346.244.252.112\u00a0-\u00a0-\u00a0[29/Jan/2024:13:46:44\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"20011823\u2581\u2581\u2503 \u2503\u258c\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b f1\u00a0Help^t\u00a0Tail^l\u00a0Line\u00a0nos.^g\u00a0Go\u00a0to\u2193\u00a0Next\u2191\u00a0PreviousTAIL29/01/2024\u00a013:34:58\u00a0\u2022\u00a02540 Frogmouth https://raw.githubusercontent.com/textualize/frogmouth/main/README.md \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\u2513 \u2503\ud83d\uddbc\u00a0\u00a0Discord\u2503ContentsLocalBookmarksHistory \u2503\u2503\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\u2501\u2501 \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\u2503 \u2503\u258e\u258a\u2503\u25bc\u00a0\u2160\u00a0Frogmouth \u2503\u258eFrogmouth\u258a\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Screenshots \u2503\u258e\u258a\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Compatibility \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\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Installing \u2503Frogmouth\u00a0is\u00a0a\u00a0Markdown\u00a0viewer\u00a0/\u00a0browser\u00a0for\u00a0your\u00a0terminal,\u00a0\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Running \u2503built\u00a0with\u00a0Textual.\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Features \u2503\u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Follow\u00a0this\u00a0project \u2503Frogmouth\u00a0can\u00a0open\u00a0*.md\u00a0files\u00a0locally\u00a0or\u00a0via\u00a0a\u00a0URL.\u00a0There\u00a0is\u00a0a\u00a0\u2503 \u2503familiar\u00a0browser-like\u00a0navigation\u00a0stack,\u00a0history,\u00a0bookmarks,\u00a0and\u2503 \u2503table\u00a0of\u00a0contents.\u2585\u2585\u2503 \u2503\u2503 \u2503A\u00a0quick\u00a0video\u00a0tour\u00a0of\u00a0Frogmouth.\u2503 \u2503\u2503 \u2503https://user-images.githubusercontent.com/554369/235305502-2699\u2503 \u2503a70e-c9a6-495e-990e-67606d84bbfa.mp4\u2503 \u2503\u2503 \u2503(thanks\u00a0Screen\u00a0Studio)\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\u2503 \u2503\u258e\u258a\u2503 \u2503\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Screenshots\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\u2503 \u2503\u258e\u258a\u2503 \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\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\u2503 \u2503\u258e\u258a\u2503 \u2503\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Compatibility\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2503 \u2503\u258e\u258a\u2503 \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\u2503 \u2503Frogmouth\u00a0runs\u00a0on\u00a0Linux,\u00a0macOS,\u00a0and\u00a0Windows.\u00a0Frogmouth\u00a0requires\u2503 \u2503Python\u00a03.8\u00a0or\u00a0above.\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\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0 TUIApp Memray\u00a0live\u00a0tracking\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tue\u00a0Feb\u00a020\u00a013:53:11\u00a02024 \u00a0(\u2229\uff40-\u00b4)\u2283\u2501\u2606\uff9f.*\uff65\uff61\uff9f\u00a0\u256d\u2500\u00a0Heap\u00a0Usage\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e PID:\u00a077542CMD:\u00a0memray\u00a0run\u00a0--live\u00a0-m\u00a0http.server\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 TID:\u00a00x1Thread\u00a01\u00a0of\u00a01\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 Samples:\u00a06Duration:\u00a06.1\u00a0seconds\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 \u2570\u2500\u2500\u00a01.501MB\u00a0(100%\u00a0of\u00a01.501MB\u00a0max)\u00a0\u2500\u256f \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Location\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Total\u00a0Bytes%\u00a0TotalOwn\u00a0Bytes%\u00a0OwnAllocations \u00a0_run_tracker\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.440MB\u00a095.94%\u00a0\u00a01.111KB0.07%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0440\u00a0memray.comman \u00a0_run_module_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.381MB\u00a091.99%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0388\u00a0<frozen\u00a0runpy \u00a0_find_and_load\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.364MB\u00a090.86%\u00a0960.000B0.06%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0361\u00a0<frozen\u00a0impor \u00a0_load_unlocked\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.360MB\u00a090.62%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0355\u00a0<frozen\u00a0impor\u2584\u2584 \u00a0exec_module\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.355MB\u00a090.28%\u00a0\u00a01.225KB0.08%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0351\u00a0<frozen\u00a0impor \u00a0run_module\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.351MB\u00a090.00%\u00a0\u00a01.273KB0.08%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0354\u00a0<frozen\u00a0runpy \u00a0_run_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.334MB\u00a088.90%\u00a0890.000B0.06%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0341\u00a0<frozen\u00a0runpy \u00a0_call_with_frames_removed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.298MB\u00a086.49%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0283\u00a0<frozen\u00a0impor \u00a0get_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.168MB\u00a077.80%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0185\u00a0<frozen\u00a0impor \u00a0<module>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.095MB\u00a072.96%\u00a0\u00a01.688KB0.11%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a095\u00a0http.server\u00a0\u00a0 \u00a0_find_and_load_unlocked\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a059.031KB\u00a0\u00a03.84%\u00a0\u00a0\u00a01.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040\u00a0<frozen\u00a0impor \u00a0test\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a042.097KB\u00a0\u00a02.74%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a027\u00a0http.server\u00a0\u00a0 \u00a0__init__\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a041.565KB\u00a0\u00a02.70%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a020\u00a0socketserver\u00a0 \u00a0getfqdn\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040.933KB\u00a0\u00a02.66%\u00a0\u00a02.135KB0.14%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a018\u00a0socket\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0server_bind\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040.933KB\u00a0\u00a02.66%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a018\u00a0http.server\u00a0\u00a0 \u00a0search_function\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a038.798KB\u00a0\u00a02.52%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a016\u00a0encodings\u00a0\u00a0\u00a0\u00a0 \u00a0_handle_fromlist\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a029.723KB\u00a0\u00a01.93%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a033\u00a0<frozen\u00a0impor \u00a0<module>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a024.617KB\u00a0\u00a01.60%\u00a0\u00a01.688KB0.11%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0encodings.idn \u00a0_compile\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a023.629KB\u00a0\u00a01.54%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a011\u00a0re\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u00a0Q\u00a0\u00a0Quit\u00a0\u00a0<\u00a0\u00a0Previous\u00a0Thread\u00a0\u00a0>\u00a0\u00a0Next\u00a0Thread\u00a0\u00a0T\u00a0\u00a0Sort\u00a0by\u00a0Total\u00a0\u00a0O\u00a0\u00a0Sort\u00a0by\u00a0Own\u00a0\u00a0A\u00a0\u00a0Sort\u00a0by\u00a0Allocations\u00a0\u00a0SPACE\u00a0\u00a0Pause\u00a0

    Harlequin\u256d\u2500\u00a0Data\u00a0Catalog\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u00a0Query\u00a0Editor\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\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u25bc\u00a0f1\u00a0db\u2502\u2502\u00a01\u00a0\u00a0select\u2502 \u2502\u2514\u2500\u00a0\u25bc\u00a0main\u00a0sch\u2502\u2502\u00a02\u00a0\u00a0drivers.surname,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0circuits\u00a0t\u2502\u2502\u00a03\u00a0\u00a0drivers.forename,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0constructor_result\u2502\u2502\u00a04\u00a0\u00a0drivers.nationality,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0constructor_standi\u2502\u2502\u00a05\u00a0\u00a0avg(driver_standings.position)asavg_standing,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0constructors\u00a0t\u2502\u2502\u00a06\u00a0\u00a0avg(driver_standings.points)asavg_points\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0driver_standings\u00a0t\u2502\u2502\u00a07\u00a0\u00a0fromdriver_standings\u2502 \u2502\u251c\u2500\u00a0\u25bc\u00a0drivers\u00a0t\u2502\u2502\u00a08\u00a0\u00a0joindriversondriver_standings.driverid=drivers.driverid\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0code\u00a0s\u2502\u2502\u00a09\u00a0\u00a0joinracesondriver_standings.raceid=races.raceid\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0dob\u00a0d\u2502\u250210\u00a0\u00a0groupby1,\u00a02,\u00a03\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0driverId\u00a0##\u2502\u250211\u00a0\u00a0orderbyavg_standing\u00a0asc\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u00a0\u00a0\u251c\u2500\u00a0driverRef\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0forename\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0nationality\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0number\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0surname\u00a0s\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\u256f \u2502\u2502\u00a0\u00a0\u2514\u2500\u00a0url\u00a0s\u2502\u2590X\u258c\u00a0Limit\u00a0500Run\u00a0Query \u2502\u251c\u2500\u00a0\u25b6\u00a0lap_times\u00a0t\u2502\u256d\u2500\u00a0Query\u00a0Results\u00a0(850\u00a0Records)\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\u256e \u2502\u251c\u2500\u00a0\u25b6\u00a0pit_stops\u00a0t\u2502\u2502\u00a0surname\u00a0s\u00a0forename\u00a0s\u00a0nationality\u00a0s\u00a0avg_standing\u00a0#.#\u00a0av\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0qualifying\u00a0t\u2502\u2502\u00a0Hamilton\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Lewis\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02.66\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a014\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0races\u00a0t\u2502\u2502\u00a0Prost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Alain\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0French\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03.51\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a033\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0results\u00a0t\u2502\u2502\u00a0Stewart\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Jackie\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03.78\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a024\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0seasons\u00a0t\u2502\u2502\u00a0Schumacher\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0German\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04.33\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a046\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0sprint_results\u00a0t\u2502\u2502\u00a0Verstappen\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Max\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dutch\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a012\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0status\u00a0t\u2502\u2502\u00a0Fangio\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Juan\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Argentine\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.22\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a016\u2502 \u2502\u2514\u2500\u00a0\u25b6\u00a0tbl1\u00a0t\u2502\u2502\u00a0Pablo\u00a0Montoya\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Juan\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Colombian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.25\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a027\u2502 \u2502\u2502\u2502\u00a0Farina\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Nino\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Italian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.27\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a011\u2502 \u2502\u2502\u2502\u00a0Hulme\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Denny\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0New\u00a0Zealander\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.34\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a014\u2502 \u2502\u2502\u2502\u00a0Fagioli\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Luigi\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Italian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.67\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a09.\u2502 \u2502\u2502\u2502\u00a0Clark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Jim\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.81\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a017\u2502 \u2502\u2502\u2502\u00a0Vettel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Sebastian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0German\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.84\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a010\u2502 \u2502\u2502\u2502\u00a0Senna\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ayrton\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Brazilian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.92\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a031\u2502 \u2502\u258c\u2502\u2502\u258c\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\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u00a0CTRL+Q\u00a0\u00a0Quit\u00a0\u00a0F1\u00a0\u00a0Help\u00a0 Stopwatch tutorialstopwatch.pystopwatch.tcss

    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
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    min-width: 50;\n    margin: 1;\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
    Pride examplepride.py

    PrideApp

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass PrideApp(App):\n    \"\"\"Displays a pride flag.\"\"\"\n\n    COLORS = [\"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"purple\"]\n\n    def compose(self) -> ComposeResult:\n        for color in self.COLORS:\n            stripe = Static()\n            stripe.styles.height = \"1fr\"\n            stripe.styles.background = color\n            yield stripe\n\n\nif __name__ == \"__main__\":\n    PrideApp().run()\n
    Calculator examplecalculator.pycalculator.tcss

    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

    \"\"\"\nAn implementation of a classic calculator, with a layout inspired by macOS calculator.\n\nWorks like a real calculator. Click the buttons or press the equivalent keys.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom textual import events, on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.css.query import NoMatches\nfrom textual.reactive import var\nfrom textual.widgets import Button, Digits\n\n\nclass CalculatorApp(App):\n    \"\"\"A working 'desktop' calculator.\"\"\"\n\n    CSS_PATH = \"calculator.tcss\"\n\n    numbers = var(\"0\")\n    show_ac = var(True)\n    left = var(Decimal(\"0\"))\n    right = var(Decimal(\"0\"))\n    value = var(\"\")\n    operator = var(\"plus\")\n\n    # Maps button IDs on to the corresponding key name\n    NAME_MAP = {\n        \"asterisk\": \"multiply\",\n        \"slash\": \"divide\",\n        \"underscore\": \"plus-minus\",\n        \"full_stop\": \"point\",\n        \"plus_minus_sign\": \"plus-minus\",\n        \"percent_sign\": \"percent\",\n        \"equals_sign\": \"equals\",\n        \"minus\": \"minus\",\n        \"plus\": \"plus\",\n    }\n\n    def watch_numbers(self, value: str) -> None:\n        \"\"\"Called when numbers is updated.\"\"\"\n        self.query_one(\"#numbers\", Digits).update(value)\n\n    def compute_show_ac(self) -> bool:\n        \"\"\"Compute switch to show AC or C button\"\"\"\n        return self.value in (\"\", \"0\") and self.numbers == \"0\"\n\n    def watch_show_ac(self, show_ac: bool) -> None:\n        \"\"\"Called when show_ac changes.\"\"\"\n        self.query_one(\"#c\").display = not show_ac\n        self.query_one(\"#ac\").display = show_ac\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Add our buttons.\"\"\"\n        with Container(id=\"calculator\"):\n            yield Digits(id=\"numbers\")\n            yield Button(\"AC\", id=\"ac\", variant=\"primary\")\n            yield Button(\"C\", id=\"c\", variant=\"primary\")\n            yield Button(\"+/-\", id=\"plus-minus\", variant=\"primary\")\n            yield Button(\"%\", id=\"percent\", variant=\"primary\")\n            yield Button(\"\u00f7\", id=\"divide\", variant=\"warning\")\n            yield Button(\"7\", id=\"number-7\", classes=\"number\")\n            yield Button(\"8\", id=\"number-8\", classes=\"number\")\n            yield Button(\"9\", id=\"number-9\", classes=\"number\")\n            yield Button(\"\u00d7\", id=\"multiply\", variant=\"warning\")\n            yield Button(\"4\", id=\"number-4\", classes=\"number\")\n            yield Button(\"5\", id=\"number-5\", classes=\"number\")\n            yield Button(\"6\", id=\"number-6\", classes=\"number\")\n            yield Button(\"-\", id=\"minus\", variant=\"warning\")\n            yield Button(\"1\", id=\"number-1\", classes=\"number\")\n            yield Button(\"2\", id=\"number-2\", classes=\"number\")\n            yield Button(\"3\", id=\"number-3\", classes=\"number\")\n            yield Button(\"+\", id=\"plus\", variant=\"warning\")\n            yield Button(\"0\", id=\"number-0\", classes=\"number\")\n            yield Button(\".\", id=\"point\")\n            yield Button(\"=\", id=\"equals\", variant=\"warning\")\n\n    def on_key(self, event: events.Key) -> None:\n        \"\"\"Called when the user presses a key.\"\"\"\n\n        def press(button_id: str) -> None:\n            \"\"\"Press a button, should it exist.\"\"\"\n            try:\n                self.query_one(f\"#{button_id}\", Button).press()\n            except NoMatches:\n                pass\n\n        key = event.key\n        if key.isdecimal():\n            press(f\"number-{key}\")\n        elif key == \"c\":\n            press(\"c\")\n            press(\"ac\")\n        else:\n            button_id = self.NAME_MAP.get(key)\n            if button_id is not None:\n                press(self.NAME_MAP.get(key, key))\n\n    @on(Button.Pressed, \".number\")\n    def number_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Pressed a number.\"\"\"\n        assert event.button.id is not None\n        number = event.button.id.partition(\"-\")[-1]\n        self.numbers = self.value = self.value.lstrip(\"0\") + number\n\n    @on(Button.Pressed, \"#plus-minus\")\n    def plus_minus_pressed(self) -> None:\n        \"\"\"Pressed + / -\"\"\"\n        self.numbers = self.value = str(Decimal(self.value or \"0\") * -1)\n\n    @on(Button.Pressed, \"#percent\")\n    def percent_pressed(self) -> None:\n        \"\"\"Pressed %\"\"\"\n        self.numbers = self.value = str(Decimal(self.value or \"0\") / Decimal(100))\n\n    @on(Button.Pressed, \"#point\")\n    def pressed_point(self) -> None:\n        \"\"\"Pressed .\"\"\"\n        if \".\" not in self.value:\n            self.numbers = self.value = (self.value or \"0\") + \".\"\n\n    @on(Button.Pressed, \"#ac\")\n    def pressed_ac(self) -> None:\n        \"\"\"Pressed AC\"\"\"\n        self.value = \"\"\n        self.left = self.right = Decimal(0)\n        self.operator = \"plus\"\n        self.numbers = \"0\"\n\n    @on(Button.Pressed, \"#c\")\n    def pressed_c(self) -> None:\n        \"\"\"Pressed C\"\"\"\n        self.value = \"\"\n        self.numbers = \"0\"\n\n    def _do_math(self) -> None:\n        \"\"\"Does the math: LEFT OPERATOR RIGHT\"\"\"\n        try:\n            if self.operator == \"plus\":\n                self.left += self.right\n            elif self.operator == \"minus\":\n                self.left -= self.right\n            elif self.operator == \"divide\":\n                self.left /= self.right\n            elif self.operator == \"multiply\":\n                self.left *= self.right\n            self.numbers = str(self.left)\n            self.value = \"\"\n        except Exception:\n            self.numbers = \"Error\"\n\n    @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\n    @on(Button.Pressed, \"#equals\")\n    def pressed_equals(self) -> None:\n        \"\"\"Pressed =\"\"\"\n        if self.value:\n            self.right = Decimal(self.value)\n        self._do_math()\n\n\nif __name__ == \"__main__\":\n    CalculatorApp().run(inline=True)\n
    Screen {\n    overflow: auto;\n}\n\n#calculator {\n    layout: grid;\n    grid-size: 4;\n    grid-gutter: 1 2;\n    grid-columns: 1fr;\n    grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr;\n    margin: 1 2;\n    min-height: 25;\n    min-width: 26;\n    height: 100%;\n\n    &:inline {\n        margin: 0 2;\n    }\n}\n\nButton {\n    width: 100%;\n    height: 100%;\n}\n\n#numbers {\n    column-span: 4;\n    padding: 0 1;\n    height: 100%;\n    background: $primary-lighten-2;\n    color: $text;\n    content-align: center middle;\n    text-align: right;\n}\n\n#number-0 {\n    column-span: 2;\n}\n
    "},{"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 comprehensive 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, particularly 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.

    Changed in version 0.80.0

    Textual added an ansi_color boolean to App. If you set this to True, then Textual will not attempt to convert ANSI colors. Note that you will lose transparency effects if you enable this setting.

    "},{"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 \u00a0^b\u00a0Sidebar\u00a0\u00a0^t\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0^s\u00a0Screenshot\u00a0\u00a0f1\u00a0Notes\u00a0\u00a0^q\u00a0Quit\u00a0\u258f^p\u00a0palette

    "},{"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/#widget-examples","title":"Widget examples","text":"

    In addition to the example apps, you can also find the code listings used to generate the screenshots in these docs in the docs/examples directory.

    "},{"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.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:08.10 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0a\u00a0Add\u00a0\u00a0r\u00a0Remove\u00a0\u258f^p\u00a0palette

    Info

    Did you notice the ^p palette at the bottom right hand corner? This is the Command Palette. You can think of it as a dedicated command prompt for your app.

    "},{"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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

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

    stopwatch01.py \u2b58StopwatchApp \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    "},{"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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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.11 \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.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 \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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.10 \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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0a\u00a0Add\u00a0\u00a0r\u00a0Remove\u00a0\u258f^p\u00a0palette

    "},{"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 Lady\u00a0Jessica 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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    "},{"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.ruff_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__\u2585\u2585 \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

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

    A footer to display and interact with key bindings.

    Footer reference

    FooterApp \u00a0q\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0del\u00a0Delete\u00a0the\u00a0thing\u00a0\u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    "},{"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 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258aMarkdown\u00a0Viewer \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a \u258aFeatures \u258a \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 \u258aTables \u258a \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a \u258aName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Type\u00a0Default\u00a0Description\u00a0\u00a0\u00a0\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u00a0 \u258ashow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258azebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u00a0\u00a0\u00a0\u00a0 \u258aheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258ashow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u258a \u258a \u258aCode\u00a0Blocks \u258a\u2585\u2585 \u258aCode\u00a0blocks\u00a0are\u00a0syntax\u00a0highlighted,\u00a0with\u00a0guidelines. \u258a \u258a \u258aclassListViewExample(App): \u258a\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u258a\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldListView(

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

    Display a markdown document.

    Markdown reference

    MarkdownExampleApp Markdown\u00a0Document This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. Features 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/#maskedinput","title":"MaskedInput","text":"

    A control to enter input according to a template mask.

    MaskedInput reference

    MaskedInputApp Enter\u00a0a\u00a0valid\u00a0credit\u00a0card\u00a0number. \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 \u258a0000-0000-0000-0000\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

    "},{"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 \u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    "},{"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\u00a0Add\u00a0tab\u00a0\u00a0r\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0c\u00a0Clear\u00a0tabs\u00a0\u258f^p\u00a0palette

    "},{"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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    "},{"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 \u258a1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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":"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 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":"textual.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 = (\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.CommandCallback","title":"CommandCallback module-attribute","text":"
    CommandCallback = (\n    \"Callable[[], Awaitable[Any]] | Callable[[], Any]\"\n)\n

    Signature for callbacks used in get_system_commands

    "},{"location":"api/app/#textual.app.ScreenType","title":"ScreenType module-attribute","text":"
    ScreenType = TypeVar('ScreenType', bound=Screen)\n

    Type var for a Screen, used in get_screen.

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

    Bases: Exception

    Base class for exceptions relating to actions.

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

    Bases: ModeError

    Raised when attempting to remove the currently active mode.

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

    Bases: Generic[ReturnType], DOMNode

    The base class for Textual Applications.

    Parameters:

    Name Type Description Default Type[Driver] | None

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

    None CSSPathType | 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.

    None bool

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

    False bool

    Allow ANSI colors if True, or convert ANSI colors to to RGB if False.

    False

    Raises:

    Type Description CssPathError

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

    "},{"location":"api/app/#textual.app.App(driver_class)","title":"driver_class","text":""},{"location":"api/app/#textual.app.App(css_path)","title":"css_path","text":""},{"location":"api/app/#textual.app.App(watch_css)","title":"watch_css","text":""},{"location":"api/app/#textual.app.App(ansi_color)","title":"ansi_color","text":""},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS = '*'\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.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True\n    )\n]\n

    The default key bindings.

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

    Set to text to show in the key panel.

    "},{"location":"api/app/#textual.app.App.CLOSE_TIMEOUT","title":"CLOSE_TIMEOUT class-attribute instance-attribute","text":"
    CLOSE_TIMEOUT = 5.0\n

    Timeout waiting for widget's to close, or None for no timeout.

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

    Command providers used by the command palette.

    Should be a set of command.Provider classes.

    "},{"location":"api/app/#textual.app.App.COMMAND_PALETTE_BINDING","title":"COMMAND_PALETTE_BINDING class-attribute","text":"
    COMMAND_PALETTE_BINDING = 'ctrl+p'\n

    The key that launches the command palette (if enabled by App.ENABLE_COMMAND_PALETTE).

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

    How the command palette key should be displayed in the footer (or None for default).

    "},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
    CSS = ''\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 = 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 = True\n

    Should the command palette be enabled for the application?

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

    Use escape key to minimize widgets (potentially overriding bindings).

    This is the default value, used if the active screen's ESCAPE_TO_MINIMIZE is not changed from None.

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

    Number of blank lines above an inline app.

    "},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
    MODES = {}\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.NOTIFICATION_TIMEOUT","title":"NOTIFICATION_TIMEOUT class-attribute","text":"
    NOTIFICATION_TIMEOUT = 5\n

    Default number of seconds to show notifications before removing them.

    "},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
    SCREENS = {}\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 = 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 = 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.TOOLTIP_DELAY","title":"TOOLTIP_DELAY class-attribute instance-attribute","text":"
    TOOLTIP_DELAY = 0.5\n

    The time in seconds after which a tooltip gets displayed.

    "},{"location":"api/app/#textual.app.App.active_bindings","title":"active_bindings property","text":"
    active_bindings\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, ActiveBinding]

    A dict that maps keys on to binding information.

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

    Determines what type of animations the app will display.

    See textual.constants.TEXTUAL_ANIMATIONS.

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

    The animator object.

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

    Allow ANSI colors in UI?

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

    The ANSI TerminalTheme currently being used.

    Defines how colors defined as ANSI (e.g. magenta) inside Rich renderables are mapped to hex codes.

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

    Maps ANSI colors to hex colors using a Rich TerminalTheme object while in dark mode.

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

    Maps ANSI colors to hex colors using a Rich TerminalTheme object while in light mode.

    "},{"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\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\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(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\n

    Is debug mode enabled?

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

    Use the escape key to minimize?

    When a widget is maximized, this boolean determines if the escape key will minimize the widget (potentially overriding any bindings).

    The default logic is to use the screen's ESCAPE_TO_MINIMIZE classvar if it is set to True or False. If the classvar on the screen is not set (and left as None), then the app's ESCAPE_TO_MINIMIZE is used.

    "},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
    focused\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_attached","title":"is_attached property","text":"
    is_attached\n

    Is this node linked to the app through the DOM?

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

    Is this a root node (i.e. the App)?

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

    Is the app running in 'headless' mode?

    Headless mode is used when running tests with run_test.

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

    Is the app running in 'inline' mode?

    "},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
    log\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.return_code","title":"return_code property","text":"
    return_code\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 hasn'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\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\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\n

    A snapshot of the current screen stack.

    Returns:

    Type Description list[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 = 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 = 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\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 = SUB_TITLE if SUB_TITLE is not None else ''\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 = TITLE if TITLE is not None else f'{__name__}'\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 = 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\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":"
    action_add_class(selector, class_name)\n

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

    Parameters:

    Name Type Description Default str

    Selects the widget to add the class to.

    required str

    The class to add to the selected widget.

    required"},{"location":"api/app/#textual.app.App.action_add_class(selector)","title":"selector","text":""},{"location":"api/app/#textual.app.App.action_add_class(class_name)","title":"class_name","text":""},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
    action_back()\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":"
    action_bell()\n

    An action to play the terminal 'bell'.

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

    Show the Textual command palette.

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

    An action to focus the given widget.

    Parameters:

    Name Type Description Default str

    ID of widget to focus.

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

    An action to focus the next widget.

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

    An action to focus the previous widget.

    "},{"location":"api/app/#textual.app.App.action_hide_help_panel","title":"action_hide_help_panel","text":"
    action_hide_help_panel()\n

    Hide the keys panel (if present).

    "},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
    action_pop_screen()\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":"
    action_push_screen(screen)\n

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

    Parameters:

    Name Type Description Default str

    Name of the screen.

    required"},{"location":"api/app/#textual.app.App.action_push_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
    action_quit()\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":"
    action_remove_class(selector, class_name)\n

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

    Parameters:

    Name Type Description Default str

    Selects the widget to remove the class from.

    required str

    The class to remove from the selected widget.

    required"},{"location":"api/app/#textual.app.App.action_remove_class(selector)","title":"selector","text":""},{"location":"api/app/#textual.app.App.action_remove_class(class_name)","title":"class_name","text":""},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot","text":"
    action_screenshot(filename=None, path=None)\n

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

    Parameters:

    Name Type Description Default str | None

    Filename of screenshot, or None to auto-generate.

    None str | None

    Path to directory. Defaults to the user's Downloads directory.

    None"},{"location":"api/app/#textual.app.App.action_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.action_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.action_show_help_panel","title":"action_show_help_panel","text":"
    action_show_help_panel()\n

    Show the keys panel.

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

    An action to simulate a key press.

    This will invoke the same actions as if the user had pressed the key.

    Parameters:

    Name Type Description Default str

    The key to process.

    required"},{"location":"api/app/#textual.app.App.action_simulate_key(key)","title":"key","text":""},{"location":"api/app/#textual.app.App.action_suspend_process","title":"action_suspend_process","text":"
    action_suspend_process()\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":"
    action_switch_mode(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":"
    action_switch_screen(screen)\n

    An action to switch screens.

    Parameters:

    Name Type Description Default str

    Name of the screen.

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

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

    Parameters:

    Name Type Description Default str

    Selects the widget to toggle the class on.

    required str

    The class to toggle on the selected widget.

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

    An action to toggle dark mode.

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

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

    Parameters:

    Name Type Description Default str

    The new mode.

    required str | Callable[[], Screen]

    The base screen associated with the given mode.

    required

    Raises:

    Type Description InvalidModeError

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

    "},{"location":"api/app/#textual.app.App.add_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.add_mode(base_screen)","title":"base_screen","text":""},{"location":"api/app/#textual.app.App.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    See the guide for how to use the animation system.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required float | Animatable

    The value to animate to.

    required object

    The final value of the animation.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/app/#textual.app.App.animate(attribute)","title":"attribute","text":""},{"location":"api/app/#textual.app.App.animate(value)","title":"value","text":""},{"location":"api/app/#textual.app.App.animate(final_value)","title":"final_value","text":""},{"location":"api/app/#textual.app.App.animate(duration)","title":"duration","text":""},{"location":"api/app/#textual.app.App.animate(speed)","title":"speed","text":""},{"location":"api/app/#textual.app.App.animate(delay)","title":"delay","text":""},{"location":"api/app/#textual.app.App.animate(easing)","title":"easing","text":""},{"location":"api/app/#textual.app.App.animate(on_complete)","title":"on_complete","text":""},{"location":"api/app/#textual.app.App.animate(level)","title":"level","text":""},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update","text":"
    batch_update()\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","text":"
    begin_capture_print(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:

    Name Type Description Default MessageTarget

    The widget where print content will be sent.

    required bool

    Capture stdout.

    True bool

    Capture stderr.

    True"},{"location":"api/app/#textual.app.App.begin_capture_print(target)","title":"target","text":""},{"location":"api/app/#textual.app.App.begin_capture_print(stdout)","title":"stdout","text":""},{"location":"api/app/#textual.app.App.begin_capture_print(stderr)","title":"stderr","text":""},{"location":"api/app/#textual.app.App.bell","title":"bell","text":"
    bell()\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","text":"
    bind(\n    keys,\n    action,\n    *,\n    description=\"\",\n    show=True,\n    key_display=None\n)\n

    Bind a key to an action.

    Parameters:

    Name Type Description Default str

    A comma separated list of keys, i.e.

    required str

    Action to bind to.

    required str

    Short description of action.

    '' bool

    Show key in UI.

    True str | None

    Replacement text for key, or None to use default.

    None"},{"location":"api/app/#textual.app.App.bind(keys)","title":"keys","text":""},{"location":"api/app/#textual.app.App.bind(action)","title":"action","text":""},{"location":"api/app/#textual.app.App.bind(description)","title":"description","text":""},{"location":"api/app/#textual.app.App.bind(show)","title":"show","text":""},{"location":"api/app/#textual.app.App.bind(key_display)","title":"key_display","text":""},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread","text":"
    call_from_thread(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:

    Name Type Description Default Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]]

    A callable to run.

    required Any

    Arguments to the callback.

    () 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.call_from_thread(callback)","title":"callback","text":""},{"location":"api/app/#textual.app.App.call_from_thread(*args)","title":"*args","text":""},{"location":"api/app/#textual.app.App.call_from_thread(**kwargs)","title":"**kwargs","text":""},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse","text":"
    capture_mouse(widget)\n

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

    Parameters:

    Name Type Description Default Widget | None

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

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

    Clear all the current notifications.

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

    Yield child widgets for a container.

    This method should be implemented in a subclass.

    "},{"location":"api/app/#textual.app.App.copy_to_clipboard","title":"copy_to_clipboard","text":"
    copy_to_clipboard(text)\n

    Copy text to the clipboard.

    Note

    This does not work on macOS Terminal, but will work on most other terminals.

    Parameters:

    Name Type Description Default str

    Text you wish to copy to the clipboard.

    required"},{"location":"api/app/#textual.app.App.copy_to_clipboard(text)","title":"text","text":""},{"location":"api/app/#textual.app.App.deliver_binary","title":"deliver_binary","text":"
    deliver_binary(\n    path_or_file,\n    *,\n    save_directory=None,\n    save_filename=None,\n    open_method=\"download\",\n    mime_type=None,\n    name=None\n)\n

    Deliver a binary file to the end-user of the application.

    If an IO object is supplied, it will be closed by this method and must not be used after it is supplied to this method.

    If running in a terminal, this will save the file to the user's downloads directory.

    If running via a web browser, this will initiate a download via a single-use URL.

    This operation runs in a thread when running on web, so this method returning does not indicate that the file has been delivered.

    After the file has been delivered, a DeliveryComplete message will be posted to this App, which contains the delivery_key returned by this method. By handling this message, you can add custom logic to your application that fires only after the file has been delivered.

    Parameters:

    Name Type Description Default str | Path | BinaryIO

    The path or file-like object to save.

    required str | Path | None

    The directory to save the file to. If None, the default \"downloads\" directory will be used. This argument is ignored when running via the web.

    None str | None

    The filename to save the file to. If None, the following logic applies to generate the filename: - If path_or_file is a file-like object, the filename will be taken from the name attribute if available. - If path_or_file is a path, the filename will be taken from the path. - If a filename is not available, a filename will be generated using the App's title and the current date and time.

    None Literal['browser', 'download']

    The method to use to open the file. \"browser\" will open the file in the web browser, \"download\" will initiate a download. Note that this can sometimes be impacted by the browser's settings.

    'download' str | None

    The MIME type of the file or None to guess based on file extension. If no MIME type is supplied and we cannot guess the MIME type, from the file extension, the MIME type will be set to \"application/octet-stream\".

    None str | None

    A user-defined named which will be returned in DeliveryComplete and DeliveryComplete.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_binary(path_or_file)","title":"path_or_file","text":""},{"location":"api/app/#textual.app.App.deliver_binary(save_directory)","title":"save_directory","text":""},{"location":"api/app/#textual.app.App.deliver_binary(save_filename)","title":"save_filename","text":""},{"location":"api/app/#textual.app.App.deliver_binary(open_method)","title":"open_method","text":""},{"location":"api/app/#textual.app.App.deliver_binary(mime_type)","title":"mime_type","text":""},{"location":"api/app/#textual.app.App.deliver_binary(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot","title":"deliver_screenshot","text":"
    deliver_screenshot(\n    filename=None, path=None, time_format=None\n)\n

    Deliver a screenshot of the app.

    This with save the screenshot when running locally, or serve it when the app is running in a web browser.

    Parameters:

    Name Type Description Default str | None

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

    None str | None

    Path to directory for output when saving locally (not used when app is running in the browser). Defaults to current working directory.

    None str | 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.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot(time_format)","title":"time_format","text":""},{"location":"api/app/#textual.app.App.deliver_text","title":"deliver_text","text":"
    deliver_text(\n    path_or_file,\n    *,\n    save_directory=None,\n    save_filename=None,\n    open_method=\"download\",\n    encoding=None,\n    mime_type=None,\n    name=None\n)\n

    Deliver a text file to the end-user of the application.

    If a TextIO object is supplied, it will be closed by this method and must not be used after this method is called.

    If running in a terminal, this will save the file to the user's downloads directory.

    If running via a web browser, this will initiate a download via a single-use URL.

    After the file has been delivered, a DeliveryComplete message will be posted to this App, which contains the delivery_key returned by this method. By handling this message, you can add custom logic to your application that fires only after the file has been delivered.

    Parameters:

    Name Type Description Default str | Path | TextIO

    The path or file-like object to save.

    required str | Path | None

    The directory to save the file to.

    None str | None

    The filename to save the file to. If path_or_file is a file-like object, the filename will be generated from the name attribute if available. If path_or_file is a path the filename will be generated from the path.

    None str | None

    The encoding to use when saving the file. If None, the encoding will be determined by supplied file-like object (if possible). If this is not possible, 'utf-8' will be used.

    None str | None

    The MIME type of the file or None to guess based on file extension. If no MIME type is supplied and we cannot guess the MIME type, from the file extension, the MIME type will be set to \"text/plain\".

    None str | None

    A user-defined named which will be returned in DeliveryComplete and DeliveryComplete.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_text(path_or_file)","title":"path_or_file","text":""},{"location":"api/app/#textual.app.App.deliver_text(save_directory)","title":"save_directory","text":""},{"location":"api/app/#textual.app.App.deliver_text(save_filename)","title":"save_filename","text":""},{"location":"api/app/#textual.app.App.deliver_text(encoding)","title":"encoding","text":""},{"location":"api/app/#textual.app.App.deliver_text(mime_type)","title":"mime_type","text":""},{"location":"api/app/#textual.app.App.deliver_text(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print","text":"
    end_capture_print(target)\n

    End capturing of prints.

    Parameters:

    Name Type Description Default MessageTarget

    The widget that was capturing prints.

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

    Exit the app, and return the supplied result.

    Parameters:

    Name Type Description Default ReturnType | None

    Return value.

    None int

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

    0 RenderableType | None

    Optional message to display on exit.

    None"},{"location":"api/app/#textual.app.App.exit(result)","title":"result","text":""},{"location":"api/app/#textual.app.App.exit(return_code)","title":"return_code","text":""},{"location":"api/app/#textual.app.App.exit(message)","title":"message","text":""},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot","text":"
    export_screenshot(*, title=None, simplify=False)\n

    Export an SVG screenshot of the current screen.

    See also save_screenshot which writes the screenshot to a file.

    Parameters:

    Name Type Description Default str | None

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

    None bool

    Simplify the segments by combining contiguous segments with the same style.

    False"},{"location":"api/app/#textual.app.App.export_screenshot(title)","title":"title","text":""},{"location":"api/app/#textual.app.App.export_screenshot(simplify)","title":"simplify","text":""},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id","text":"
    get_child_by_id(id: str) -> Widget\n
    get_child_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_child_by_id(id, expect_type=None)\n

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

    Parameters:

    Name Type Description Default str

    The ID of the node to search for.

    required type[ExpectType] | None

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

    None

    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_id(id)","title":"id","text":""},{"location":"api/app/#textual.app.App.get_child_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type","text":"
    get_child_by_type(expect_type)\n

    Get a child of a give type.

    Parameters:

    Name Type Description Default type[ExpectType]

    The type of the expected child.

    required

    Raises:

    Type Description NoMatches

    If no valid child is found.

    Returns:

    Type Description ExpectType

    A widget.

    "},{"location":"api/app/#textual.app.App.get_child_by_type(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables","text":"
    get_css_variables()\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_default_screen","title":"get_default_screen","text":"
    get_default_screen()\n

    Get the default screen.

    This is called when the App is first composed. The returned screen instance will be the first screen on the stack.

    Implement this method if you would like to use a custom Screen as the default screen.

    Returns:

    Type Description Screen

    A screen instance.

    "},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class","text":"
    get_driver_class()\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","text":"
    get_key_display(binding)\n

    Format a bound key for display in footer / key panel etc.

    Note

    You can implement this in a subclass if you want to change how keys are displayed in your app.

    Parameters:

    Name Type Description Default Binding

    A Binding.

    required

    Returns:

    Type Description str

    A string used to represent the key.

    "},{"location":"api/app/#textual.app.App.get_key_display(binding)","title":"binding","text":""},{"location":"api/app/#textual.app.App.get_loading_widget","title":"get_loading_widget","text":"
    get_loading_widget()\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_pseudo_classes","title":"get_pseudo_classes","text":"
    get_pseudo_classes()\n

    Pseudo classes for a widget.

    Returns:

    Type Description Iterable[str]

    Names of the pseudo classes.

    "},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen","text":"
    get_screen(screen: ScreenType) -> ScreenType\n
    get_screen(screen: str) -> Screen\n
    get_screen(\n    screen: str,\n    screen_class: Type[ScreenType] | None = None,\n) -> ScreenType\n
    get_screen(\n    screen: ScreenType,\n    screen_class: Type[ScreenType] | None = None,\n) -> ScreenType\n
    get_screen(screen, screen_class=None)\n

    Get an installed screen.

    Example
    my_screen = self.get_screen(\"settings\", MyScreen)\n

    Parameters:

    Name Type Description Default Screen | str

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

    required Type[Screen] | None

    Class of expected screen, or None for any screen class.

    None

    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_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.get_screen(screen_class)","title":"screen_class","text":""},{"location":"api/app/#textual.app.App.get_system_commands","title":"get_system_commands","text":"
    get_system_commands(screen)\n

    A generator of system commands used in the command palette.

    Parameters:

    Name Type Description Default Screen

    The screen where the command palette was invoked from.

    required

    Implement this method in your App subclass if you want to add custom commands. Here is an example:

    def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:\n    yield from super().get_system_commands(screen)\n    yield SystemCommand(\"Bell\", \"Ring the bell\", self.bell)\n

    Note

    Requires that SystemCommandsProvider is in App.COMMANDS class variable.

    Yields:

    Type Description Iterable[SystemCommand]

    SystemCommand instances.

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

    Get the widget under the given coordinates.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description tuple[Widget, Region]

    The widget and the widget's screen region.

    "},{"location":"api/app/#textual.app.App.get_widget_at(x)","title":"x","text":""},{"location":"api/app/#textual.app.App.get_widget_at(y)","title":"y","text":""},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id","text":"
    get_widget_by_id(id: str) -> Widget\n
    get_widget_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_widget_by_id(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:

    Name Type Description Default str

    The ID to search for in the subtree

    required type[ExpectType] | None

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

    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.get_widget_by_id(id)","title":"id","text":""},{"location":"api/app/#textual.app.App.get_widget_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.handle_bindings_clash","title":"handle_bindings_clash","text":"
    handle_bindings_clash(clashed_bindings, node)\n

    Handle a clash between bindings.

    Bindings clashes are likely due to users setting conflicting keys via their keymap.

    This method is intended to be overridden by subclasses.

    Textual will call this each time a clash is encountered - which may be on each keypress if a clashing widget is focused or is in the bindings chain.

    Parameters:

    Name Type Description Default set[Binding]

    The bindings that are clashing.

    required DOMNode

    The node that has the clashing bindings.

    required"},{"location":"api/app/#textual.app.App.handle_bindings_clash(clashed_bindings)","title":"clashed_bindings","text":""},{"location":"api/app/#textual.app.App.handle_bindings_clash(node)","title":"node","text":""},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen","text":"
    install_screen(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:

    Name Type Description Default Screen

    Screen to install.

    required str

    Unique name to identify the screen.

    required

    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.install_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.install_screen(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted","text":"
    is_mounted(widget)\n

    Check if a widget is mounted.

    Parameters:

    Name Type Description Default Widget

    A widget.

    required

    Returns:

    Type Description bool

    True of the widget is mounted.

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

    Check if a given screen has been installed.

    Parameters:

    Name Type Description Default Screen | str

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

    required

    Returns:

    Type Description bool

    True if the screen is currently installed,

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

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

    Parameters:

    Name Type Description Default Widget

    The widget(s) to mount.

    () int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(*widgets)","title":"*widgets","text":""},{"location":"api/app/#textual.app.App.mount(before)","title":"before","text":""},{"location":"api/app/#textual.app.App.mount(after)","title":"after","text":""},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all","text":"
    mount_all(widgets, *, before=None, after=None)\n

    Mount widgets from an iterable.

    Parameters:

    Name Type Description Default Iterable[Widget]

    An iterable of widgets.

    required int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(widgets)","title":"widgets","text":""},{"location":"api/app/#textual.app.App.mount_all(before)","title":"before","text":""},{"location":"api/app/#textual.app.App.mount_all(after)","title":"after","text":""},{"location":"api/app/#textual.app.App.notify","title":"notify","text":"
    notify(\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=None\n)\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters:

    Name Type Description Default str

    The message for the notification.

    required str

    The title for the notification.

    '' SeverityLevel

    The severity of the notification.

    'information' float | None

    The timeout (in seconds) for the notification, or None for default.

    None

    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.notify(message)","title":"message","text":""},{"location":"api/app/#textual.app.App.notify(title)","title":"title","text":""},{"location":"api/app/#textual.app.App.notify(severity)","title":"severity","text":""},{"location":"api/app/#textual.app.App.notify(timeout)","title":"timeout","text":""},{"location":"api/app/#textual.app.App.open_url","title":"open_url","text":"
    open_url(url, *, new_tab=True)\n

    Open a URL in the default web browser.

    Parameters:

    Name Type Description Default str

    The URL to open.

    required bool

    Whether to open the URL in a new tab.

    True"},{"location":"api/app/#textual.app.App.open_url(url)","title":"url","text":""},{"location":"api/app/#textual.app.App.open_url(new_tab)","title":"new_tab","text":""},{"location":"api/app/#textual.app.App.panic","title":"panic","text":"
    panic(*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:

    Name Type Description Default RenderableType

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

    ()"},{"location":"api/app/#textual.app.App.panic(*renderables)","title":"*renderables","text":""},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen","text":"
    pop_screen()\n

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

    Returns:

    Type Description AwaitComplete

    The screen that was replaced.

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

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

    "},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen","text":"
    push_screen(\n    screen: Screen[ScreenResultType] | str,\n    callback: (\n        ScreenResultCallbackType[ScreenResultType] | None\n    ) = None,\n    wait_for_dismiss: Literal[False] = False,\n) -> AwaitMount\n
    push_screen(\n    screen: Screen[ScreenResultType] | str,\n    callback: (\n        ScreenResultCallbackType[ScreenResultType] | None\n    ) = None,\n    wait_for_dismiss: Literal[True] = True,\n) -> Future[ScreenResultType]\n
    push_screen(screen, callback=None, wait_for_dismiss=False)\n

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

    Parameters:

    Name Type Description Default Screen[ScreenResultType] | str

    A Screen instance or the name of an installed screen.

    required ScreenResultCallbackType[ScreenResultType] | None

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

    None bool

    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.

    False

    Raises:

    Type Description NoActiveWorker

    If using wait_for_dismiss outside of a worker.

    Returns:

    Type Description AwaitMount | 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(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.push_screen(callback)","title":"callback","text":""},{"location":"api/app/#textual.app.App.push_screen(wait_for_dismiss)","title":"wait_for_dismiss","text":""},{"location":"api/app/#textual.app.App.push_screen_wait","title":"push_screen_wait async","text":"
    push_screen_wait(\n    screen: Screen[ScreenResultType],\n) -> ScreenResultType\n
    push_screen_wait(screen: str) -> Any\n
    push_screen_wait(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:

    Name Type Description Default Screen[ScreenResultType] | str

    A screen or the name of an installed screen.

    required

    Returns:

    Type Description ScreenResultType | Any

    The screen's result.

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

    Recompose the widget.

    Recomposing will remove children and call self.compose again to remount.

    "},{"location":"api/app/#textual.app.App.refresh","title":"refresh","text":"
    refresh(*, repaint=True, layout=False, recompose=False)\n

    Refresh the entire screen.

    Parameters:

    Name Type Description Default bool

    Repaint the widget (will call render() again).

    True bool

    Also layout widgets in the view.

    False bool

    Re-compose the widget (will remove and re-mount children).

    False

    Returns:

    Type Description Self

    The App instance.

    "},{"location":"api/app/#textual.app.App.refresh(repaint)","title":"repaint","text":""},{"location":"api/app/#textual.app.App.refresh(layout)","title":"layout","text":""},{"location":"api/app/#textual.app.App.refresh(recompose)","title":"recompose","text":""},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css","text":"
    refresh_css(animate=True)\n

    Refresh CSS.

    Parameters:

    Name Type Description Default bool

    Also execute CSS animations.

    True"},{"location":"api/app/#textual.app.App.refresh_css(animate)","title":"animate","text":""},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode","text":"
    remove_mode(mode)\n

    Removes a mode from the app.

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

    Parameters:

    Name Type Description Default str

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

    required

    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.remove_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.render","title":"render","text":"
    render()\n

    Render method, inherited from widget, to render the screen's background.

    May be overridden to customize background visuals.

    "},{"location":"api/app/#textual.app.App.run","title":"run","text":"
    run(\n    *,\n    headless=False,\n    inline=False,\n    inline_no_clear=False,\n    mouse=True,\n    size=None,\n    auto_pilot=None\n)\n

    Run the app.

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output).

    False bool

    Run the app inline (under the prompt).

    False bool

    Don't clear the app output when exiting an inline app.

    False bool

    Enable mouse support.

    True tuple[int, int] | None

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

    None AutopilotCallbackType | None

    An auto pilot coroutine.

    None

    Returns:

    Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run(inline)","title":"inline","text":""},{"location":"api/app/#textual.app.App.run(inline_no_clear)","title":"inline_no_clear","text":""},{"location":"api/app/#textual.app.App.run(mouse)","title":"mouse","text":""},{"location":"api/app/#textual.app.App.run(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run(auto_pilot)","title":"auto_pilot","text":""},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
    run_action(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:

    Name Type Description Default str | ActionParseResult

    Action encoded in a string.

    required DOMNode | None

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

    None

    Returns:

    Type Description bool

    True if the event has been handled.

    "},{"location":"api/app/#textual.app.App.run_action(action)","title":"action","text":""},{"location":"api/app/#textual.app.App.run_action(default_namespace)","title":"default_namespace","text":""},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
    run_async(\n    *,\n    headless=False,\n    inline=False,\n    inline_no_clear=False,\n    mouse=True,\n    size=None,\n    auto_pilot=None\n)\n

    Run the app asynchronously.

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output).

    False bool

    Run the app inline (under the prompt).

    False bool

    Don't clear the app output when exiting an inline app.

    False bool

    Enable mouse support.

    True tuple[int, int] | None

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

    None AutopilotCallbackType | None

    An autopilot coroutine.

    None

    Returns:

    Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_async(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run_async(inline)","title":"inline","text":""},{"location":"api/app/#textual.app.App.run_async(inline_no_clear)","title":"inline_no_clear","text":""},{"location":"api/app/#textual.app.App.run_async(mouse)","title":"mouse","text":""},{"location":"api/app/#textual.app.App.run_async(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run_async(auto_pilot)","title":"auto_pilot","text":""},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
    run_test(\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:

    ```python\nasync with app.run_test() as pilot:\n    await pilot.click(\"#Button.ok\")\n    assert ...\n```\n

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output or input).

    True tuple[int, int] | None

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

    (80, 24) bool

    Enable tooltips when testing.

    False bool

    Enable notifications when testing.

    False Callable[[Message], None] | None

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

    None"},{"location":"api/app/#textual.app.App.run_test(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run_test(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run_test(tooltips)","title":"tooltips","text":""},{"location":"api/app/#textual.app.App.run_test(notifications)","title":"notifications","text":""},{"location":"api/app/#textual.app.App.run_test(message_hook)","title":"message_hook","text":""},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot","text":"
    save_screenshot(filename=None, path=None, time_format=None)\n

    Save an SVG screenshot of the current screen.

    Parameters:

    Name Type Description Default str | None

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

    None str | None

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

    None str | 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.

    None

    Returns:

    Type Description str

    Filename of screenshot.

    "},{"location":"api/app/#textual.app.App.save_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.save_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.save_screenshot(time_format)","title":"time_format","text":""},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus","text":"
    set_focus(widget, scroll_visible=True)\n

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

    Parameters:

    Name Type Description Default Widget | None

    Widget to focus.

    required bool

    Scroll widget in to view.

    True"},{"location":"api/app/#textual.app.App.set_focus(widget)","title":"widget","text":""},{"location":"api/app/#textual.app.App.set_focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/app/#textual.app.App.set_keymap","title":"set_keymap","text":"
    set_keymap(keymap)\n

    Set the keymap, a mapping of binding IDs to key strings.

    Bindings in the keymap are used to override default key bindings, i.e. those defined in BINDINGS class variables.

    Bindings with IDs that are present in the keymap will have their key string replaced with the value from the keymap.

    Parameters:

    Name Type Description Default Keymap

    A mapping of binding IDs to key strings.

    required"},{"location":"api/app/#textual.app.App.set_keymap(keymap)","title":"keymap","text":""},{"location":"api/app/#textual.app.App.simulate_key","title":"simulate_key","text":"
    simulate_key(key)\n

    Simulate a key press.

    This will perform the same action as if the user had pressed the key.

    Parameters:

    Name Type Description Default str

    Key to simulate. May also be the name of a key, e.g. \"space\".

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

    Stop an animation on an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute whose animation should be stopped.

    required bool

    Should the animation be set to its final value?

    True Note

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

    "},{"location":"api/app/#textual.app.App.stop_animation(attribute)","title":"attribute","text":""},{"location":"api/app/#textual.app.App.stop_animation(complete)","title":"complete","text":""},{"location":"api/app/#textual.app.App.suspend","title":"suspend","text":"
    suspend()\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","text":"
    switch_mode(mode)\n

    Switch to a given mode.

    Parameters:

    Name Type Description Default str

    The mode to switch to.

    required

    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_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen","text":"
    switch_screen(screen)\n

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

    Parameters:

    Name Type Description Default Screen | str

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

    required"},{"location":"api/app/#textual.app.App.switch_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen","text":"
    uninstall_screen(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:

    Name Type Description Default Screen | str

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

    required

    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.uninstall_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.update_keymap","title":"update_keymap","text":"
    update_keymap(keymap)\n

    Update the App's keymap, merging with keymap.

    If a Binding ID exists in both the App's keymap and the keymap argument, the keymap argument takes precedence.

    Parameters:

    Name Type Description Default Keymap

    A mapping of binding IDs to key strings.

    required"},{"location":"api/app/#textual.app.App.update_keymap(keymap)","title":"keymap","text":""},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles","text":"
    update_styles(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","text":"
    validate_sub_title(sub_title)\n

    Make sure the subtitle is set to a string.

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

    Make sure the title is set to a string.

    "},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark","text":"
    watch_dark(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","text":"

    Bases: Exception

    Base class for general App related exceptions.

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

    Bases: ModeError

    Raised if there is an issue with a mode name.

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

    Bases: Exception

    Base class for exceptions related to modes.

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

    Bases: Exception

    Base class for exceptions that relate to screens.

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

    Bases: ScreenError

    Raised when trying to manipulate the screen stack incorrectly.

    "},{"location":"api/app/#textual.app.SuspendNotSupported","title":"SuspendNotSupported","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.SystemCommand","title":"SystemCommand","text":"

    Bases: NamedTuple

    Defines a system command used in the command palette (yielded from get_system_commands).

    "},{"location":"api/app/#textual.app.SystemCommand.callback","title":"callback instance-attribute","text":"
    callback\n

    A callback to invoke when the command is selected.

    "},{"location":"api/app/#textual.app.SystemCommand.discover","title":"discover class-attribute instance-attribute","text":"
    discover = True\n

    Should the command show when the search is empty?

    "},{"location":"api/app/#textual.app.SystemCommand.help","title":"help instance-attribute","text":"
    help\n

    Additional help text, shown under the title.

    "},{"location":"api/app/#textual.app.SystemCommand.title","title":"title instance-attribute","text":"
    title\n

    The title of the command (used in search).

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

    Bases: ModeError

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

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

    Callable to lazy load the system commands.

    Returns:

    Type Description type[SystemCommandsProvider]

    System commands class.

    "},{"location":"api/await_complete/","title":"textual.await_complete","text":"

    This module contains the AwaitComplete class. An AwaitComplete object is returned by methods that do work in the background. You can await this object if you need to know when that work has completed. Or you can ignore it, and Textual will automatically await the work before handling the next message.

    Note

    You are unlikely to need to explicitly create these objects yourself.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete","title":"AwaitComplete","text":"
    AwaitComplete(*awaitables, pre_await=None)\n

    An 'optionally-awaitable' object which runs one or more coroutines (or other awaitables) concurrently.

    Parameters:

    Name Type Description Default Awaitable

    One or more awaitables to run concurrently.

    ()"},{"location":"api/await_complete/#textual.await_complete.AwaitComplete(awaitables)","title":"awaitables","text":""},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.exception","title":"exception property","text":"
    exception\n

    An exception if the awaitables failed.

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

    True if the task has completed.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.call_next","title":"call_next","text":"
    call_next(node)\n

    Await after the next message.

    Parameters:

    Name Type Description Default MessagePump

    The node which created the object.

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

    Returns an already completed instance of AwaitComplete.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.set_pre_await_callback","title":"set_pre_await_callback","text":"
    set_pre_await_callback(pre_await)\n

    Set a callback to run prior to awaiting.

    This is used by Textual, mainly to check for possible deadlocks. You are unlikely to need to call this method in an app.

    Parameters:

    Name Type Description Default CallbackType | None

    A callback.

    required"},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.set_pre_await_callback(pre_await)","title":"pre_await","text":""},{"location":"api/await_remove/","title":"textual.await_remove","text":"

    This module contains the AwaitRemove class. An AwaitRemove object is returned by Widget.remove() and other methods which remove widgets. You can await the return value if you need to know exactly when the widget(s) have been removed. Or you can ignore it and Textual will wait for the widgets to be removed before handling the next message.

    Note

    You are unlikely to need to explicitly create these objects yourself.

    An optionally awaitable object returned by methods that remove widgets.

    "},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove","text":"
    AwaitRemove(tasks, post_remove=None)\n

    An awaitable that waits for nodes to be removed.

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

    This module contains the Binding class and related objects.

    See bindings in the guide for details.

    "},{"location":"api/binding/#textual.binding.BindingIDString","title":"BindingIDString module-attribute","text":"
    BindingIDString = str\n

    The ID of a Binding defined somewhere in the application.

    Corresponds to the id parameter of the Binding class.

    "},{"location":"api/binding/#textual.binding.BindingType","title":"BindingType module-attribute","text":"
    BindingType = (\n    \"Binding | tuple[str, str] | tuple[str, str, str]\"\n)\n

    The possible types of a binding found in the BINDINGS class variable.

    "},{"location":"api/binding/#textual.binding.KeyString","title":"KeyString module-attribute","text":"
    KeyString = str\n

    A string that represents a key binding.

    For example, \"x\", \"ctrl+i\", \"ctrl+shift+a\", \"ctrl+j,space,x\", etc.

    "},{"location":"api/binding/#textual.binding.Keymap","title":"Keymap module-attribute","text":"
    Keymap = Mapping[BindingIDString, KeyString]\n

    A mapping of binding IDs to key strings, used for overriding default key bindings.

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

    Bases: NamedTuple

    Information about an active binding (returned from active_bindings).

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

    The binding information.

    "},{"location":"api/binding/#textual.binding.ActiveBinding.enabled","title":"enabled instance-attribute","text":"
    enabled\n

    Is the binding enabled? (enabled bindings are typically rendered dim)

    "},{"location":"api/binding/#textual.binding.ActiveBinding.node","title":"node instance-attribute","text":"
    node\n

    The node where the binding is defined.

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

    Optional tooltip shown in Footer.

    "},{"location":"api/binding/#textual.binding.Binding","title":"Binding dataclass","text":"
    Binding(\n    key,\n    action,\n    description=\"\",\n    show=True,\n    key_display=None,\n    priority=False,\n    tooltip=\"\",\n    id=None,\n)\n

    The configuration of a key binding.

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

    Action to bind to.

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

    Description of action.

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

    ID of the binding. Intended to be globally unique, but uniqueness is not enforced.

    If specified in the App's keymap then Textual will use this ID to lookup the binding, and substitute the key property of the Binding with the key specified in the keymap.

    "},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
    key\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 = None\n

    How the key should be shown in footer.

    If None, the display of the key will use the result of App.get_key_display.

    If overridden in a keymap then this value is ignored.

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

    Enable priority binding for this key.

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

    Show the action in Footer, or False to hide.

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

    Optional tooltip to show in footer.

    "},{"location":"api/binding/#textual.binding.Binding.make_bindings","title":"make_bindings classmethod","text":"
    make_bindings(bindings)\n

    Convert a list of BindingType (the types that can be specified in BINDINGS) into an Iterable[Binding].

    Compound bindings like \"j,down\" will be expanded into 2 Binding instances.

    Parameters:

    Name Type Description Default Iterable[BindingType]

    An iterable of BindingType.

    required

    Returns:

    Type Description Iterable[Binding]

    An iterable of Binding.

    "},{"location":"api/binding/#textual.binding.Binding.make_bindings(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.Binding.parse_key","title":"parse_key","text":"
    parse_key()\n

    Parse a key in to a list of modifiers, and the actual key.

    Returns:

    Type Description tuple[list[str], str]

    A tuple of (MODIFIER LIST, KEY).

    "},{"location":"api/binding/#textual.binding.Binding.with_key","title":"with_key","text":"
    with_key(key, key_display=None)\n

    Return a new binding with the key and key_display set to the specified values.

    Parameters:

    Name Type Description Default str

    The new key to set.

    required str | None

    The new key display to set.

    None

    Returns:

    Type Description Binding

    A new binding with the key set to the specified value.

    "},{"location":"api/binding/#textual.binding.Binding.with_key(key)","title":"key","text":""},{"location":"api/binding/#textual.binding.Binding.with_key(key_display)","title":"key_display","text":""},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError","text":"

    Bases: Exception

    A binding related error.

    "},{"location":"api/binding/#textual.binding.BindingsMap","title":"BindingsMap","text":"
    BindingsMap(bindings=None)\n

    Manage a set of bindings.

    Parameters:

    Name Type Description Default Iterable[BindingType] | None

    An optional set of initial bindings.

    None Note

    The iterable of bindings can contain either a Binding instance, or a tuple of 3 values mapping to the first three properties of a Binding.

    "},{"location":"api/binding/#textual.binding.BindingsMap(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.BindingsMap.key_to_bindings","title":"key_to_bindings instance-attribute","text":"
    key_to_bindings = {}\n

    Mapping of key (e.g. \"ctrl+a\") to list of bindings for that key.

    "},{"location":"api/binding/#textual.binding.BindingsMap.shown_keys","title":"shown_keys property","text":"
    shown_keys\n

    A list of bindings for shown keys.

    "},{"location":"api/binding/#textual.binding.BindingsMap.apply_keymap","title":"apply_keymap","text":"
    apply_keymap(keymap)\n

    Replace bindings for keys that are present in keymap.

    Preserves existing bindings for keys that are not in keymap.

    Parameters:

    Name Type Description Default Keymap

    A keymap to overlay.

    required

    Returns:

    Name Type Description KeymapApplyResult KeymapApplyResult

    The result of applying the keymap, including any clashed bindings.

    "},{"location":"api/binding/#textual.binding.BindingsMap.apply_keymap(keymap)","title":"keymap","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind","title":"bind","text":"
    bind(\n    keys,\n    action,\n    description=\"\",\n    show=True,\n    key_display=None,\n    priority=False,\n)\n

    Bind keys to an action.

    Parameters:

    Name Type Description Default str

    The keys to bind. Can be a comma-separated list of keys.

    required str

    The action to bind the keys to.

    required str

    An optional description for the binding.

    '' bool

    A flag to say if the binding should appear in the footer.

    True str | None

    Optional string to display in the footer for the key.

    None bool

    Is this a priority binding, checked form app down to focused widget?

    False"},{"location":"api/binding/#textual.binding.BindingsMap.bind(keys)","title":"keys","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(action)","title":"action","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(description)","title":"description","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(show)","title":"show","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(key_display)","title":"key_display","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(priority)","title":"priority","text":""},{"location":"api/binding/#textual.binding.BindingsMap.copy","title":"copy","text":"
    copy()\n

    Return a copy of this instance.

    Return

    New bindings object.

    "},{"location":"api/binding/#textual.binding.BindingsMap.from_keys","title":"from_keys classmethod","text":"
    from_keys(keys)\n

    Construct a BindingsMap from a dict of keys and bindings.

    Parameters:

    Name Type Description Default dict[str, list[Binding]]

    A dict that maps a key on to a list of Binding objects.

    required

    Returns:

    Type Description BindingsMap

    New BindingsMap

    "},{"location":"api/binding/#textual.binding.BindingsMap.from_keys(keys)","title":"keys","text":""},{"location":"api/binding/#textual.binding.BindingsMap.get_bindings_for_key","title":"get_bindings_for_key","text":"
    get_bindings_for_key(key)\n

    Get a list of bindings for a given key.

    Parameters:

    Name Type Description Default str

    Key to look up.

    required

    Raises:

    Type Description NoBinding

    If the binding does not exist.

    Returns:

    Type Description list[Binding]

    A list of bindings associated with the key.

    "},{"location":"api/binding/#textual.binding.BindingsMap.get_bindings_for_key(key)","title":"key","text":""},{"location":"api/binding/#textual.binding.BindingsMap.merge","title":"merge classmethod","text":"
    merge(bindings)\n

    Merge a bindings.

    Parameters:

    Name Type Description Default Iterable[BindingsMap]

    A number of bindings.

    required

    Returns:

    Type Description BindingsMap

    New BindingsMap.

    "},{"location":"api/binding/#textual.binding.BindingsMap.merge(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding","text":"

    Bases: Exception

    Binding key is in an invalid format.

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

    Bases: NamedTuple

    The result of applying a keymap.

    "},{"location":"api/binding/#textual.binding.KeymapApplyResult.clashed_bindings","title":"clashed_bindings instance-attribute","text":"
    clashed_bindings\n

    A list of bindings that were clashed and replaced by the keymap.

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

    Bases: Exception

    A binding was not found.

    "},{"location":"api/cache/","title":"textual.cache","text":"

    Cache classes are dict-like containers used to avoid recalculating expensive operations such as rendering.

    You can also use them in your own apps for similar reasons.

    "},{"location":"api/cache/#textual.cache.FIFOCache","title":"FIFOCache","text":"
    FIFOCache(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:

    Name Type Description Default int

    Maximum size of cache before discarding items.

    required"},{"location":"api/cache/#textual.cache.FIFOCache(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.FIFOCache.clear","title":"clear","text":"
    clear()\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.FIFOCache.get","title":"get","text":"
    get(key: CacheKey) -> CacheValue | None\n
    get(\n    key: CacheKey, default: DefaultValue\n) -> CacheValue | DefaultValue\n
    get(key, default=None)\n

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

    Parameters:

    Name Type Description Default CacheKey

    Key

    required DefaultValue | None

    Default to return if key is not present.

    None

    Returns:

    Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.FIFOCache.get(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.FIFOCache.get(default)","title":"default","text":""},{"location":"api/cache/#textual.cache.FIFOCache.keys","title":"keys","text":"
    keys()\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.FIFOCache.set","title":"set","text":"
    set(key, value)\n

    Set a value.

    Parameters:

    Name Type Description Default CacheKey

    Key.

    required CacheValue

    Value.

    required"},{"location":"api/cache/#textual.cache.FIFOCache.set(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.FIFOCache.set(value)","title":"value","text":""},{"location":"api/cache/#textual.cache.LRUCache","title":"LRUCache","text":"
    LRUCache(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:

    Name Type Description Default int

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

    required"},{"location":"api/cache/#textual.cache.LRUCache(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.LRUCache.maxsize","title":"maxsize property writable","text":"
    maxsize\n
    "},{"location":"api/cache/#textual.cache.LRUCache.clear","title":"clear","text":"
    clear()\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.LRUCache.discard","title":"discard","text":"
    discard(key)\n

    Discard item in cache from key.

    Parameters:

    Name Type Description Default CacheKey

    Cache key.

    required"},{"location":"api/cache/#textual.cache.LRUCache.discard(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.get","title":"get","text":"
    get(key: CacheKey) -> CacheValue | None\n
    get(\n    key: CacheKey, default: DefaultValue\n) -> CacheValue | DefaultValue\n
    get(key, default=None)\n

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

    Parameters:

    Name Type Description Default CacheKey

    Key

    required DefaultValue | None

    Default to return if key is not present.

    None

    Returns:

    Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.LRUCache.get(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.get(default)","title":"default","text":""},{"location":"api/cache/#textual.cache.LRUCache.grow","title":"grow","text":"
    grow(maxsize)\n

    Grow the maximum size to at least maxsize elements.

    Parameters:

    Name Type Description Default int

    New maximum size.

    required"},{"location":"api/cache/#textual.cache.LRUCache.grow(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.LRUCache.keys","title":"keys","text":"
    keys()\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.LRUCache.set","title":"set","text":"
    set(key, value)\n

    Set a value.

    Parameters:

    Name Type Description Default CacheKey

    Key.

    required CacheValue

    Value.

    required"},{"location":"api/cache/#textual.cache.LRUCache.set(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.set(value)","title":"value","text":""},{"location":"api/color/","title":"textual.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 = Color(0, 0, 0)\n

    A constant for pure black.

    "},{"location":"api/color/#textual.color.TRANSPARENT","title":"TRANSPARENT module-attribute","text":"
    TRANSPARENT = parse('transparent')\n

    A constant for transparent.

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

    A constant for pure white.

    "},{"location":"api/color/#textual.color.Color","title":"Color","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 = 1.0\n

    Alpha (opacity) component in range 0 to 1.

    "},{"location":"api/color/#textual.color.Color.ansi","title":"ansi class-attribute instance-attribute","text":"
    ansi = None\n

    ANSI color index. -1 means default color. None if not an ANSI color.

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

    Blue component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
    brightness\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\n

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

    "},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
    css\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\n

    Green component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
    hex\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\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\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\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\n

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

    "},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
    monochrome\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\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\n

    Red component in range 0 to 255.

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

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

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

    This color encoded in Rich's Color class.

    Returns:

    Type Description Color

    A color object as used by Rich.

    "},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
    blend(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:

    Name Type Description Default Color

    Another color.

    required float

    A blend factor, 0 -> 1.

    required float | None

    New alpha for result.

    None

    Returns:

    Type Description Color

    A new color.

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

    Darken the color by a given amount.

    Parameters:

    Name Type Description Default float

    Value between 0-1 to reduce luminance by.

    required float | None

    Alpha component for new color or None to copy alpha.

    None

    Returns:

    Type Description Color

    New color.

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

    Create a color from HLS components.

    Parameters:

    Name Type Description Default float

    Hue.

    required float

    Lightness.

    required float

    Saturation.

    required

    Returns:

    Type Description Color

    A new color.

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

    Create a new color from Rich's Color class.

    Parameters:

    Name Type Description Default Color

    An instance of Rich color.

    required

    Returns:

    Type Description Color

    A new Color instance.

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

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

    Parameters:

    Name Type Description Default float

    An alpha value to apply to the result.

    0.95

    Returns:

    Type Description Color

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

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

    Lighten the color by a given amount.

    Parameters:

    Name Type Description Default float

    Value between 0-1 to increase luminance by.

    required float | None

    Alpha component for new color or None to copy alpha.

    None

    Returns:

    Type Description Color

    New color.

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

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

    Parameters:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.multiply_alpha(alpha)","title":"alpha","text":""},{"location":"api/color/#textual.color.Color.parse","title":"parse cached classmethod","text":"
    parse(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:

    Name Type Description Default str | Color

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

    required

    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.parse(color_text)","title":"color_text","text":""},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha","text":"
    with_alpha(alpha)\n

    Create a new color with the given alpha.

    Parameters:

    Name Type Description Default float

    New value for alpha.

    required

    Returns:

    Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.with_alpha(alpha)","title":"alpha","text":""},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError","text":"
    ColorParseError(message, suggested_color=None)\n

    Bases: Exception

    A color failed to parse.

    Parameters:

    Name Type Description Default str

    The error message

    required str | None

    A close color we can suggest.

    None"},{"location":"api/color/#textual.color.ColorParseError(message)","title":"message","text":""},{"location":"api/color/#textual.color.ColorParseError(suggested_color)","title":"suggested_color","text":""},{"location":"api/color/#textual.color.Gradient","title":"Gradient","text":"
    Gradient(*stops, quality=50)\n

    Defines a color gradient.

    A gradient is defined by a sequence of \"stops\" consisting of a tuple containing a float and a color. The stop indicates the color at that point on a spectrum between 0 and 1. Colors may be given as a Color instance, or a string that can be parsed into a Color (with Color.parse).

    The quality argument defines the number of steps in the gradient. Intermediate colors are interpolated from the two nearest colors. Increasing quality can generate a smoother looking gradient, at the expense of a little extra work to pre-calculate the colors.

    Parameters:

    Name Type Description Default tuple[float, Color | str]

    Color stops.

    () int

    The number of steps in the gradient.

    50

    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(stops)","title":"stops","text":""},{"location":"api/color/#textual.color.Gradient(quality)","title":"quality","text":""},{"location":"api/color/#textual.color.Gradient.colors","title":"colors property","text":"
    colors\n

    A list of colors in the gradient.

    "},{"location":"api/color/#textual.color.Gradient.from_colors","title":"from_colors classmethod","text":"
    from_colors(*colors, quality=50)\n

    Construct a gradient form a sequence of colors, where the stops are evenly spaced.

    Parameters:

    Name Type Description Default Color | str

    Positional arguments may be Color instances or strings to parse into a color.

    () int

    The number of steps in the gradient.

    50

    Returns:

    Type Description Gradient

    A new Gradient instance.

    "},{"location":"api/color/#textual.color.Gradient.from_colors(*colors)","title":"*colors","text":""},{"location":"api/color/#textual.color.Gradient.from_colors(quality)","title":"quality","text":""},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color","text":"
    get_color(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:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A Textual color.

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

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

    Positions that are between stops will return a blended color.

    Parameters:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A (Rich) color.

    "},{"location":"api/color/#textual.color.Gradient.get_rich_color(position)","title":"position","text":""},{"location":"api/color/#textual.color.HSL","title":"HSL","text":"

    Bases: NamedTuple

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

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

    HSL in css format.

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

    Hue in range 0 to 1.

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

    Lightness in range 0 to 1.

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

    Saturation in range 0 to 1.

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

    Bases: NamedTuple

    A color in HSV (Hue, Saturation, Value) format.

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

    Hue in range 0 to 1.

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

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
    v\n

    Value un range 0 to 1.

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

    Bases: NamedTuple

    A color in CIE-L*ab format.

    "},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
    L\n

    Lightness in range 0 to 100.

    "},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
    a\n

    A axis in range -127 to 128.

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

    B axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb","text":"
    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","text":"
    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":"textual.command","text":"

    This module contains classes for working with Textual's command palette.

    See the guide on the Command Palette for full details.

    "},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
    Hits = AsyncIterator['DiscoveryHit | Hit']\n

    Return type for the command provider's search method.

    "},{"location":"api/command/#textual.command.Command","title":"Command","text":"
    Command(prompt, hit, id=None, disabled=False)\n

    Bases: Option

    Class that holds a hit in the CommandList.

    Parameters:

    Name Type Description Default RenderableType

    The prompt for the option.

    required DiscoveryHit | Hit

    The details of the hit associated with the option.

    required str | None

    The optional ID for the option.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"api/command/#textual.command.Command(prompt)","title":"prompt","text":""},{"location":"api/command/#textual.command.Command(hit)","title":"hit","text":""},{"location":"api/command/#textual.command.Command(id)","title":"id","text":""},{"location":"api/command/#textual.command.Command(disabled)","title":"disabled","text":""},{"location":"api/command/#textual.command.Command.hit","title":"hit instance-attribute","text":"
    hit = hit\n

    The details of the hit associated with the option.

    "},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput","text":"
    CommandInput(\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    tooltip=None\n)\n

    Bases: Input

    The command palette input control.

    Parameters:

    Name Type Description Default str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Highlighter | None

    An optional highlighter for the input.

    None bool

    Flag to say if the field should obfuscate its content.

    False str | None

    A regex to restrict character inputs.

    None InputType

    The type of the input.

    'text' int

    The maximum length of the input, or 0 for no maximum length.

    0 Suggester | None

    Suggester associated with this input instance.

    None Validator | Iterable[Validator] | None

    An iterable of validators that the Input value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"api/command/#textual.command.CommandInput(value)","title":"value","text":""},{"location":"api/command/#textual.command.CommandInput(placeholder)","title":"placeholder","text":""},{"location":"api/command/#textual.command.CommandInput(highlighter)","title":"highlighter","text":""},{"location":"api/command/#textual.command.CommandInput(password)","title":"password","text":""},{"location":"api/command/#textual.command.CommandInput(restrict)","title":"restrict","text":""},{"location":"api/command/#textual.command.CommandInput(type)","title":"type","text":""},{"location":"api/command/#textual.command.CommandInput(max_length)","title":"max_length","text":""},{"location":"api/command/#textual.command.CommandInput(suggester)","title":"suggester","text":""},{"location":"api/command/#textual.command.CommandInput(validators)","title":"validators","text":""},{"location":"api/command/#textual.command.CommandInput(validate_on)","title":"validate_on","text":""},{"location":"api/command/#textual.command.CommandInput(valid_empty)","title":"valid_empty","text":""},{"location":"api/command/#textual.command.CommandInput(name)","title":"name","text":""},{"location":"api/command/#textual.command.CommandInput(id)","title":"id","text":""},{"location":"api/command/#textual.command.CommandInput(classes)","title":"classes","text":""},{"location":"api/command/#textual.command.CommandInput(disabled)","title":"disabled","text":""},{"location":"api/command/#textual.command.CommandInput(tooltip)","title":"tooltip","text":""},{"location":"api/command/#textual.command.CommandList","title":"CommandList","text":"
    CommandList(\n    *content,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    wrap=True,\n    tooltip=None,\n)\n

    Bases: OptionList

    The command palette command list.

    "},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette","text":"
    CommandPalette()\n

    Bases: SystemModalScreen

    The Textual command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"ctrl+end, shift+end\",\n        \"command_list('last')\",\n        \"Go to bottom\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+home, shift+home\",\n        \"command_list('first')\",\n        \"Go to top\",\n        show=False,\n    ),\n    Binding(\n        \"down\", \"cursor_down\", \"Next command\", show=False\n    ),\n    Binding(\"escape\", \"escape\", \"Exit the command palette\"),\n    Binding(\n        \"pagedown\",\n        \"command_list('page_down')\",\n        \"Next page\",\n        show=False,\n    ),\n    Binding(\n        \"pageup\",\n        \"command_list('page_up')\",\n        \"Previous page\",\n        show=False,\n    ),\n    Binding(\n        \"up\",\n        \"command_list('cursor_up')\",\n        \"Previous command\",\n        show=False,\n    ),\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 = {\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 = 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.Closed","title":"Closed dataclass","text":"
    Closed(option_selected)\n

    Bases: Message

    Posted to App when the command palette is closed.

    "},{"location":"api/command/#textual.command.CommandPalette.Closed.option_selected","title":"option_selected instance-attribute","text":"
    option_selected\n

    True if an option was selected, False if the palette was closed without selecting an option.

    "},{"location":"api/command/#textual.command.CommandPalette.Opened","title":"Opened dataclass","text":"
    Opened()\n

    Bases: Message

    Posted to App when the command palette is opened.

    "},{"location":"api/command/#textual.command.CommandPalette.OptionHighlighted","title":"OptionHighlighted dataclass","text":"
    OptionHighlighted(highlighted_event)\n

    Bases: Message

    Posted to App when an option is highlighted in the command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.OptionHighlighted.highlighted_event","title":"highlighted_event instance-attribute","text":"
    highlighted_event\n

    The option highlighted event from the OptionList within the command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
    is_open(app)\n

    Is the command palette current open?

    Parameters:

    Name Type Description Default App

    The app to test.

    required

    Returns:

    Type Description bool

    True if the command palette is currently open, False if not.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open(app)","title":"app","text":""},{"location":"api/command/#textual.command.DiscoveryHit","title":"DiscoveryHit dataclass","text":"
    DiscoveryHit(display, command, text=None, help=None)\n

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.DiscoveryHit.command","title":"command instance-attribute","text":"
    command\n

    The function to call when the command is chosen.

    "},{"location":"api/command/#textual.command.DiscoveryHit.display","title":"display instance-attribute","text":"
    display\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.DiscoveryHit.help","title":"help class-attribute instance-attribute","text":"
    help = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.DiscoveryHit.prompt","title":"prompt property","text":"
    prompt\n

    The prompt to use when displaying the discovery hit in the command palette.

    "},{"location":"api/command/#textual.command.DiscoveryHit.score","title":"score property","text":"
    score\n

    A discovery hit always has a score of 0.

    The order in which discovery hits are displayed is determined by the order in which they are yielded by the Provider. It's up to the developer to yield DiscoveryHits in the .

    "},{"location":"api/command/#textual.command.DiscoveryHit.text","title":"text class-attribute instance-attribute","text":"
    text = None\n

    The command text associated with the hit, as plain text.

    If display is not simple text, this attribute should be provided by the Provider object.

    "},{"location":"api/command/#textual.command.Hit","title":"Hit dataclass","text":"
    Hit(score, match_display, command, text=None, help=None)\n

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
    command\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 = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
    match_display\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.Hit.prompt","title":"prompt property","text":"
    prompt\n

    The prompt to use when displaying the hit in the command palette.

    "},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
    score\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 = 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","text":"
    Matcher(query, *, match_style=None, case_sensitive=False)\n

    A fuzzy matcher.

    Parameters:

    Name Type Description Default str

    A query as typed in by the user.

    required Style | None

    The style to use to highlight matched portions of a string.

    None bool

    Should matching be case sensitive?

    False"},{"location":"api/command/#textual.command.Matcher(query)","title":"query","text":""},{"location":"api/command/#textual.command.Matcher(match_style)","title":"match_style","text":""},{"location":"api/command/#textual.command.Matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/command/#textual.command.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive\n

    Is this matcher case sensitive?

    "},{"location":"api/command/#textual.command.Matcher.match_style","title":"match_style property","text":"
    match_style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/command/#textual.command.Matcher.query","title":"query property","text":"
    query\n

    The query string to look for.

    "},{"location":"api/command/#textual.command.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern\n

    The regular expression pattern built from the query.

    "},{"location":"api/command/#textual.command.Matcher.highlight","title":"highlight","text":"
    highlight(candidate)\n

    Highlight the candidate with the fuzzy match.

    Parameters:

    Name Type Description Default str

    The candidate string to match against the query.

    required

    Returns:

    Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/command/#textual.command.Matcher.highlight(candidate)","title":"candidate","text":""},{"location":"api/command/#textual.command.Matcher.match","title":"match","text":"
    match(candidate)\n

    Match the candidate against the query.

    Parameters:

    Name Type Description Default str

    Candidate string to match against the query.

    required

    Returns:

    Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/command/#textual.command.Matcher.match(candidate)","title":"candidate","text":""},{"location":"api/command/#textual.command.Provider","title":"Provider","text":"
    Provider(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:

    Name Type Description Default Screen[Any]

    A reference to the active screen.

    required"},{"location":"api/command/#textual.command.Provider(screen)","title":"screen","text":""},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
    app\n

    A reference to the application.

    "},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
    focused\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\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\n

    The currently-active screen in the application.

    "},{"location":"api/command/#textual.command.Provider.discover","title":"discover async","text":"
    discover()\n

    A default collection of hits for the provider.

    Yields:

    Type Description Hits

    Instances of DiscoveryHit.

    Note

    This is different from search in that it should yield DiscoveryHits that should be shown by default (before user input).

    It is permitted to not implement this method.

    "},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher","text":"
    matcher(user_input, case_sensitive=False)\n

    Create a fuzzy matcher for the given user input.

    Parameters:

    Name Type Description Default str

    The text that the user has input.

    required bool

    Should matching be case sensitive?

    False

    Returns:

    Type Description Matcher

    A fuzzy matcher object for matching against candidate hits.

    "},{"location":"api/command/#textual.command.Provider.matcher(user_input)","title":"user_input","text":""},{"location":"api/command/#textual.command.Provider.matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/command/#textual.command.Provider.search","title":"search abstractmethod async","text":"
    search(query)\n

    A request to search for commands relevant to the given query.

    Parameters:

    Name Type Description Default str

    The user input to be matched.

    required

    Yields:

    Type Description Hits

    Instances of Hit.

    "},{"location":"api/command/#textual.command.Provider.search(query)","title":"query","text":""},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
    shutdown()\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":"
    startup()\n

    Called after the Provider is initialized, but before any calls to search.

    "},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon","text":"
    SearchIcon(\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: 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('\ud83d\udd0e')\n

    The icon to display.

    "},{"location":"api/constants/","title":"textual.constants","text":"

    This module contains constants, which may be set in environment variables.

    "},{"location":"api/constants/#textual.constants.COLOR_SYSTEM","title":"COLOR_SYSTEM module-attribute","text":"
    COLOR_SYSTEM = get_environ('TEXTUAL_COLOR_SYSTEM', 'auto')\n

    Force color system override.

    "},{"location":"api/constants/#textual.constants.DEBUG","title":"DEBUG module-attribute","text":"
    DEBUG = _get_environ_bool('TEXTUAL_DEBUG')\n

    Enable debug mode.

    "},{"location":"api/constants/#textual.constants.DEVTOOLS_HOST","title":"DEVTOOLS_HOST module-attribute","text":"
    DEVTOOLS_HOST = get_environ(\n    \"TEXTUAL_DEVTOOLS_HOST\", \"127.0.0.1\"\n)\n

    The host where textual console is running.

    "},{"location":"api/constants/#textual.constants.DEVTOOLS_PORT","title":"DEVTOOLS_PORT module-attribute","text":"
    DEVTOOLS_PORT = _get_environ_int(\n    \"TEXTUAL_DEVTOOLS_PORT\", 8081\n)\n

    Constant with the port that the devtools will connect to.

    "},{"location":"api/constants/#textual.constants.DRIVER","title":"DRIVER module-attribute","text":"
    DRIVER = get_environ('TEXTUAL_DRIVER', None)\n

    Import for replacement driver.

    "},{"location":"api/constants/#textual.constants.ESCAPE_DELAY","title":"ESCAPE_DELAY module-attribute","text":"
    ESCAPE_DELAY = _get_environ_int('ESCDELAY', 100) / 1000.0\n

    The delay (in seconds) before reporting an escape key (not used if the extend key protocol is available).

    "},{"location":"api/constants/#textual.constants.FILTERS","title":"FILTERS module-attribute","text":"
    FILTERS = get_environ('TEXTUAL_FILTERS', '')\n

    A list of filters to apply to renderables.

    "},{"location":"api/constants/#textual.constants.LOG_FILE","title":"LOG_FILE module-attribute","text":"
    LOG_FILE = get_environ('TEXTUAL_LOG', None)\n

    A last resort log file that appends all logs, when devtools isn't working.

    "},{"location":"api/constants/#textual.constants.MAX_FPS","title":"MAX_FPS module-attribute","text":"
    MAX_FPS = _get_environ_int('TEXTUAL_FPS', 60)\n

    Maximum frames per second for updates.

    "},{"location":"api/constants/#textual.constants.PRESS","title":"PRESS module-attribute","text":"
    PRESS = get_environ('TEXTUAL_PRESS', '')\n

    Keys to automatically press.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_DELAY","title":"SCREENSHOT_DELAY module-attribute","text":"
    SCREENSHOT_DELAY = _get_environ_int(\n    \"TEXTUAL_SCREENSHOT\", -1\n)\n

    Seconds delay before taking screenshot.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_FILENAME","title":"SCREENSHOT_FILENAME module-attribute","text":"
    SCREENSHOT_FILENAME = get_environ(\n    \"TEXTUAL_SCREENSHOT_FILENAME\"\n)\n

    The filename to use for the screenshot.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_LOCATION","title":"SCREENSHOT_LOCATION module-attribute","text":"
    SCREENSHOT_LOCATION = get_environ(\n    \"TEXTUAL_SCREENSHOT_LOCATION\"\n)\n

    The location where screenshots should be written.

    "},{"location":"api/constants/#textual.constants.SHOW_RETURN","title":"SHOW_RETURN module-attribute","text":"
    SHOW_RETURN = _get_environ_bool('TEXTUAL_SHOW_RETURN')\n

    Write the return value on exit.

    "},{"location":"api/constants/#textual.constants.SLOW_THRESHOLD","title":"SLOW_THRESHOLD module-attribute","text":"
    SLOW_THRESHOLD = _get_environ_int(\n    \"TEXTUAL_SLOW_THRESHOLD\", 500\n)\n

    The time threshold (in milliseconds) after which a warning is logged if message processing exceeds this duration.

    "},{"location":"api/constants/#textual.constants.TEXTUAL_ANIMATIONS","title":"TEXTUAL_ANIMATIONS module-attribute","text":"
    TEXTUAL_ANIMATIONS = _get_textual_animations()\n

    Determines whether animations run or not.

    "},{"location":"api/containers/","title":"textual.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","text":"
    Center(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container which aligns children on the X axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Center(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Center(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Center(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Center(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Center(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Container","title":"Container","text":"
    Container(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    Simple container widget, with vertical layout.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Container(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Container(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Container(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Container(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Container(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Grid","title":"Grid","text":"
    Grid(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with grid layout.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Grid(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Grid(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Grid(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Grid(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Grid(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal","text":"
    Horizontal(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with horizontal layout and no scrollbars.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Horizontal(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Horizontal(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Horizontal(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Horizontal(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Horizontal(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll","text":"
    HorizontalScroll(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A container with horizontal layout and an automatic scrollbar on the X axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.HorizontalScroll(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Middle","title":"Middle","text":"
    Middle(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container which aligns children on the Y axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Middle(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Middle(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Middle(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Middle(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Middle(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer","text":"
    ScrollableContainer(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A scrollable container with vertical layout, and auto scrollbars on both axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.ScrollableContainer(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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    Binding(\n        \"ctrl+pageup\", \"page_left\", \"Page Left\", show=False\n    ),\n    Binding(\n        \"ctrl+pagedown\",\n        \"page_right\",\n        \"Page Right\",\n        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. ctrl+pageup Scroll left one page, if horizontal scrolling is available. ctrl+pagedown Scroll right one page, if horizontal scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical","text":"
    Vertical(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with vertical layout and no scrollbars.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Vertical(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Vertical(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Vertical(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Vertical(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Vertical(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll","text":"
    VerticalScroll(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A container with vertical layout and an automatic scrollbar on the Y axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.VerticalScroll(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(disabled)","title":"disabled","text":""},{"location":"api/coordinate/","title":"textual.coordinate","text":"

    A class to store a coordinate, used by the DataTable.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate","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\n

    The column of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
    row\n

    The row of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down","text":"
    down()\n

    Get the coordinate below.

    Returns:

    Type Description Coordinate

    The coordinate below.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left","text":"
    left()\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","text":"
    right()\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","text":"
    up()\n

    Get the coordinate above.

    Returns:

    Type Description Coordinate

    The coordinate above.

    "},{"location":"api/dom_node/","title":"textual.dom","text":"

    The module contains DOMNode, the base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

    "},{"location":"api/dom_node/#textual.dom.QueryOneCacheKey","title":"QueryOneCacheKey module-attribute","text":"
    QueryOneCacheKey = 'tuple[int, str, Type[Widget] | None]'\n

    The key used to cache query_one results.

    "},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
    WalkMethod = Literal['depth', 'breadth']\n

    Valid walking methods for the DOMNode.walk_children method.

    "},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier","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","text":"

    Bases: Exception

    Base exception class for errors relating to the DOM.

    "},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode","text":"
    DOMNode(*, 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.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = []\n

    A list of key bindings.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.BINDING_GROUP_TITLE","title":"BINDING_GROUP_TITLE class-attribute instance-attribute","text":"
    BINDING_GROUP_TITLE = None\n

    Title of widget used where bindings are displayed (such as in the key panel).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = set()\n

    Virtual DOM nodes, used to expose styles to line API widgets.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.DEFAULT_CLASSES","title":"DEFAULT_CLASSES class-attribute","text":"
    DEFAULT_CLASSES = ''\n

    Default classes argument if not supplied.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.DEFAULT_CSS","title":"DEFAULT_CSS class-attribute","text":"
    DEFAULT_CSS = ''\n

    Default TCSS.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.HELP","title":"HELP class-attribute","text":"
    HELP = None\n

    Optional help text shown in help panel (Markdown format).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
    SCOPED_CSS = True\n

    Should default css be limited to the widget type?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
    ancestors\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\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\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\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\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\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\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\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\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\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\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\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\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\n

    Is the node a modal?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.is_on_screen","title":"is_on_screen property","text":"
    is_on_screen\n

    Check if the node was displayed in the last screen update.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
    name\n

    The name of the node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
    parent\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\n

    A (frozen) set of all pseudo classes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
    rich_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\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\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\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\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\n

    The app's worker manager. Shortcut for self.app.workers.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.action_toggle","title":"action_toggle async","text":"
    action_toggle(attribute_name)\n

    Toggle an attribute on the node.

    Assumes the attribute is a bool.

    Parameters:

    Name Type Description Default str

    Name of the attribute.

    required"},{"location":"api/dom_node/#textual.dom.DOMNode.action_toggle(attribute_name)","title":"attribute_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class","text":"
    add_class(*class_names, update=True)\n

    Add class names to this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to add.

    () bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.add_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.add_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.automatic_refresh","title":"automatic_refresh","text":"
    automatic_refresh()\n

    Perform an automatic refresh.

    This method is called when you set the auto_refresh attribute. You could implement this method if you want to perform additional work during an automatic refresh.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_action","title":"check_action","text":"
    check_action(action, parameters)\n

    Check whether an action is enabled.

    Implement this method to add logic for dynamic actions / bindings.

    Parameters:

    Name Type Description Default str

    The name of an action.

    required tuple[object, ...]

    A tuple of any action parameters.

    required

    Returns:

    Type Description bool | None

    True if the action is enabled+visible, False if the action is disabled+hidden, None if the action is disabled+visible (grayed out in footer)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_action(action)","title":"action","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_action(parameters)","title":"parameters","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character)\n

    Check if the widget may consume the given key.

    This should be implemented in widgets that handle Key events and stop propagation (such as Input and TextArea).

    Implementing this method will hide key bindings from the footer and key panel that would be consumed by the focused widget.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    required

    Returns:

    Type Description bool

    True if the widget may capture the key in its Key event handler, or False if it won't.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key(key)","title":"key","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key(character)","title":"character","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.data_bind","title":"data_bind","text":"
    data_bind(*reactives, **bind_vars)\n

    Bind reactive data so that changes to a reactive automatically change the reactive on another widget.

    Reactives may be given as positional arguments or keyword arguments. See the guide on data binding.

    Example
    def compose(self) -> ComposeResult:\n    yield WorldClock(\"Europe/London\").data_bind(WorldClockApp.time)\n    yield WorldClock(\"Europe/Paris\").data_bind(WorldClockApp.time)\n    yield WorldClock(\"Asia/Tokyo\").data_bind(WorldClockApp.time)\n

    Raises:

    Type Description ReactiveError

    If the data wasn't bound.

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles","text":"
    get_component_styles(*names)\n

    Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

    Parameters:

    Name Type Description Default str

    Names of the components.

    ()

    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_component_styles(names)","title":"names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes","text":"
    get_pseudo_classes()\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","text":"
    has_class(*class_names)\n

    Check if the Node has all the given class names.

    Parameters:

    Name Type Description Default 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_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class","text":"
    has_pseudo_class(class_name)\n

    Check the node has the given pseudo class.

    Parameters:

    Name Type Description Default str

    The pseudo class to check for.

    required

    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_class(class_name)","title":"class_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_classes","title":"has_pseudo_classes","text":"
    has_pseudo_classes(class_names)\n

    Check the node has all the given pseudo classes.

    Parameters:

    Name Type Description Default set[str]

    Set of class names to check for.

    required

    Returns:

    Type Description bool

    True if all pseudo class names are present.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_classes(class_names)","title":"class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.mutate_reactive","title":"mutate_reactive","text":"
    mutate_reactive(reactive)\n

    Force an update to a mutable reactive.

    Example
    self.reactive_name_list.append(\"Jessica\")\nself.mutate_reactive(MyClass.reactive_name_list)\n

    Textual will automatically detect when a reactive is set to a new value, but it is unable to detect if a value is mutated (such as updating a list, dict, or attribute of an object). If you do wish to use a collection or other mutable object in a reactive, then you can call this method after your reactive is updated. This will ensure that all the reactive superpowers work.

    Note

    This method will cause watchers to be called, even if the value hasn't changed.

    Parameters:

    Name Type Description Default Reactive[ReactiveType]

    A reactive property (use the class scope syntax, i.e. MyClass.my_reactive).

    required"},{"location":"api/dom_node/#textual.dom.DOMNode.mutate_reactive(reactive)","title":"reactive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\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","text":"
    query(selector: str | None = None) -> DOMQuery[Widget]\n
    query(selector: type[QueryType]) -> DOMQuery[QueryType]\n
    query(selector=None)\n

    Query the DOM for children that match a selector or widget type.

    Parameters:

    Name Type Description Default str | type[QueryType] | None

    A CSS selector, widget type, or None for all nodes.

    None

    Returns:

    Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_children","title":"query_children","text":"
    query_children(\n    selector: str | None = None,\n) -> DOMQuery[Widget]\n
    query_children(\n    selector: type[QueryType],\n) -> DOMQuery[QueryType]\n
    query_children(selector=None)\n

    Query the DOM for the immediate children that match a selector or widget type.

    Note that this will not return child widgets more than a single level deep. If you want to a query to potentially match all children in the widget tree, see query.

    Parameters:

    Name Type Description Default str | type[QueryType] | None

    A CSS selector, widget type, or None for all nodes.

    None

    Returns:

    Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_children(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one","title":"query_exactly_one","text":"
    query_exactly_one(selector: str) -> Widget\n
    query_exactly_one(selector: type[QueryType]) -> QueryType\n
    query_exactly_one(\n    selector: str, expect_type: type[QueryType]\n) -> QueryType\n
    query_exactly_one(selector, expect_type=None)\n

    Get a widget from this widget's children that matches a selector or widget type.

    Note

    This method is similar to query_one. The only difference is that it will raise TooManyMatches if there is more than a single match.

    Parameters:

    Name Type Description Default str | type[QueryType]

    A selector or widget type.

    required type[QueryType] | None

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

    None

    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 (and exactly_one==True).

    Returns:

    Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one(expect_type)","title":"expect_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one","text":"
    query_one(selector: str) -> Widget\n
    query_one(selector: type[QueryType]) -> QueryType\n
    query_one(\n    selector: str, expect_type: type[QueryType]\n) -> QueryType\n
    query_one(selector, expect_type=None)\n

    Get a widget from this widget's children that matches a selector or widget type.

    Parameters:

    Name Type Description Default str | type[QueryType]

    A selector or widget type.

    required type[QueryType] | None

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

    None

    Raises:

    Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    Returns:

    Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_one(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_one(expect_type)","title":"expect_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.refresh_bindings","title":"refresh_bindings","text":"
    refresh_bindings()\n

    Call to prompt widgets such as the Footer to update the display of key bindings.

    See actions for how to use this method.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class","text":"
    remove_class(*class_names, update=True)\n

    Remove class names from this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to remove.

    () bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles","text":"
    reset_styles()\n

    Reset styles back to their initial state.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker","text":"
    run_worker(\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:

    Name Type Description Default WorkType[ResultType]

    A function, async function, or an awaitable object to run in a worker.

    required str | None

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' str

    A longer string to store longer information on the worker.

    '' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Start the worker immediately.

    True bool

    Cancel all workers in the same group.

    False bool

    Mark the worker as a thread worker.

    False

    Returns:

    Type Description Worker[ResultType]

    New Worker instance.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(work)","title":"work","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(name)","title":"name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(group)","title":"group","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(description)","title":"description","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(start)","title":"start","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(exclusive)","title":"exclusive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(thread)","title":"thread","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class","text":"
    set_class(add, *class_names, update=True)\n

    Add or remove class(es) based on a condition.

    Parameters:

    Name Type Description Default bool

    Add the classes if True, otherwise remove them.

    required bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_class(add)","title":"add","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes","text":"
    set_classes(classes)\n

    Replace all classes.

    Parameters:

    Name Type Description Default str | Iterable[str]

    A string containing space separated classes, or an iterable of class names.

    required

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes(classes)","title":"classes","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive","title":"set_reactive","text":"
    set_reactive(reactive, value)\n

    Sets a reactive value without invoking validators or watchers.

    Example
    self.set_reactive(App.dark_mode, True)\n

    Parameters:

    Name Type Description Default Reactive[ReactiveType]

    A reactive property (use the class scope syntax, i.e. MyClass.my_reactive).

    required ReactiveType

    New value of reactive.

    required

    Raises:

    Type Description AttributeError

    If the first argument is not a reactive.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive(reactive)","title":"reactive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive(value)","title":"value","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles","text":"
    set_styles(css=None, **update_styles)\n

    Set custom styles on this object.

    Parameters:

    Name Type Description Default str | None

    Styles in CSS format.

    None Any

    Keyword arguments map style names onto style values.

    {}

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles(css)","title":"css","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles(update_styles)","title":"update_styles","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children","title":"sort_children","text":"
    sort_children(*, key=None, reverse=False)\n

    Sort child widgets with an optional key function.

    If key is not provided then widgets will be sorted in the order they are constructed.

    Example
    # Sort widgets by name\nscreen.sort_children(key=lambda widget: widget.name or \"\")\n

    Parameters:

    Name Type Description Default Callable[[Widget], SupportsRichComparison] | None

    A callable which accepts a widget and returns something that can be sorted, or None to sort without a key function.

    None bool

    Sort in descending order.

    False"},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children(key)","title":"key","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children(reverse)","title":"reverse","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class","text":"
    toggle_class(*class_names)\n

    Toggle class names on this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to toggle.

    ()

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children","text":"
    walk_children(\n    filter_type: type[WalkType],\n    *,\n    with_self: bool = False,\n    method: WalkMethod = \"depth\",\n    reverse: bool = False\n) -> list[WalkType]\n
    walk_children(\n    *,\n    with_self: bool = False,\n    method: WalkMethod = \"depth\",\n    reverse: bool = False\n) -> list[DOMNode]\n
    walk_children(\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:

    Name Type Description Default type[WalkType] | None

    Filter only this type, or None for no filter.

    None bool

    Also yield self in addition to descendants.

    False WalkMethod

    One of \"depth\" or \"breadth\".

    'depth' bool

    Reverse the order (bottom up).

    False

    Returns:

    Type Description list[DOMNode] | list[WalkType]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(filter_type)","title":"filter_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(with_self)","title":"with_self","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(method)","title":"method","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(reverse)","title":"reverse","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch","text":"
    watch(obj, attribute_name, callback, init=True)\n

    Watches for modifications to reactive attributes on another object.

    Example
    def on_dark_change(old_value:bool, new_value:bool) -> None:\n    # Called when app.dark changes.\n    print(\"App.dark went from {old_value} to {new_value}\")\n\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n

    Parameters:

    Name Type Description Default DOMNode

    Object containing attribute to watch.

    required str

    Attribute to watch.

    required WatchCallbackType

    A callback to run when attribute changes.

    required bool

    Check watchers on first call.

    True"},{"location":"api/dom_node/#textual.dom.DOMNode.watch(obj)","title":"obj","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(attribute_name)","title":"attribute_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(callback)","title":"callback","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(init)","title":"init","text":""},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen","text":"

    Bases: DOMError

    Raised when the node has no associated screen.

    "},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers","text":"
    check_identifiers(description, *names)\n

    Validate identifier and raise an error if it fails.

    Parameters:

    Name Type Description Default str

    Description of where identifier is used for error message.

    required str

    Identifiers to check.

    ()"},{"location":"api/dom_node/#textual.dom.check_identifiers(description)","title":"description","text":""},{"location":"api/dom_node/#textual.dom.check_identifiers(*names)","title":"*names","text":""},{"location":"api/errors/","title":"textual.errors","text":"

    General exception classes.

    "},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers","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","text":"

    Bases: TextualError

    Specified widget was not found.

    "},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError","text":"

    Bases: TextualError

    An object could not be rendered.

    "},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError","text":"

    Bases: Exception

    Base class for Textual errors.

    "},{"location":"api/events/","title":"textual.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","text":"
    AppBlur()\n

    Bases: Event

    Sent when the app loses focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusOut, or when running via textual-web.

    "},{"location":"api/events/#textual.events.AppFocus","title":"AppFocus","text":"
    AppFocus()\n

    Bases: Event

    Sent when the app has focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusIn, or when running via textual-web.

    "},{"location":"api/events/#textual.events.Blur","title":"Blur","text":"
    Blur()\n

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Callback","title":"Callback","text":"
    Callback(callback)\n

    Bases: Event

    Sent by Textual to invoke a callback (see call_next and call_later).

    "},{"location":"api/events/#textual.events.Click","title":"Click","text":"
    Click(\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: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Compose","title":"Compose","text":"
    Compose()\n

    Bases: Event

    Sent to a widget to request it to compose and mount children.

    This event is used internally by Textual. You won't typically need to explicitly handle it,

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.CursorPosition","title":"CursorPosition dataclass","text":"
    CursorPosition(x, y)\n

    Bases: Event

    Internal event used to retrieve the terminal's cursor position.

    "},{"location":"api/events/#textual.events.DeliveryComplete","title":"DeliveryComplete dataclass","text":"
    DeliveryComplete(key, path=None, name=None)\n

    Bases: Event

    Sent to App when a file has been delivered.

    "},{"location":"api/events/#textual.events.DeliveryComplete.key","title":"key instance-attribute","text":"
    key\n

    The delivery key associated with the delivery.

    This is the same key that was returned by App.deliver_text/App.deliver_binary.

    "},{"location":"api/events/#textual.events.DeliveryComplete.name","title":"name class-attribute instance-attribute","text":"
    name = None\n

    Optional name returned to the app to identify the download.

    "},{"location":"api/events/#textual.events.DeliveryComplete.path","title":"path class-attribute instance-attribute","text":"
    path = None\n

    The path where the file was saved, or None if the path is not available, for example if the file was delivered via web browser.

    "},{"location":"api/events/#textual.events.DeliveryFailed","title":"DeliveryFailed dataclass","text":"
    DeliveryFailed(key, exception, name=None)\n

    Bases: Event

    Sent to App when a file delivery fails.

    "},{"location":"api/events/#textual.events.DeliveryFailed.exception","title":"exception instance-attribute","text":"
    exception\n

    The exception that was raised during the delivery.

    "},{"location":"api/events/#textual.events.DeliveryFailed.key","title":"key instance-attribute","text":"
    key\n

    The delivery key associated with the delivery.

    "},{"location":"api/events/#textual.events.DeliveryFailed.name","title":"name class-attribute instance-attribute","text":"
    name = None\n

    Optional name returned to the app to identify the download.

    "},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur dataclass","text":"
    DescendantBlur(widget)\n

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
    control\n

    The widget that was blurred (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was blurred.

    "},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus dataclass","text":"
    DescendantFocus(widget)\n

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
    control\n

    The widget that was focused (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was focused.

    "},{"location":"api/events/#textual.events.Enter","title":"Enter","text":"
    Enter(node)\n

    Bases: Event

    Sent when the mouse is moved over a widget.

    Note that this event bubbles, so a widget may receive this event when the mouse moves over a child widget. Check the node attribute for the widget directly under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Enter.node","title":"node instance-attribute","text":"
    node = node\n

    The node directly under the mouse.

    "},{"location":"api/events/#textual.events.Event","title":"Event","text":"
    Event()\n

    Bases: Message

    The base class for all events.

    "},{"location":"api/events/#textual.events.Focus","title":"Focus","text":"
    Focus()\n

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Hide","title":"Hide","text":"
    Hide()\n

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    Sent when any of the following conditions apply:

    • The widget is removed from the DOM.
    • The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
    • The widget has its display attribute set to False.
    • The widget's display style is set to \"none\".
    "},{"location":"api/events/#textual.events.Idle","title":"Idle","text":"
    Idle()\n

    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","text":"
    InputEvent()\n

    Bases: Event

    Base class for input events.

    "},{"location":"api/events/#textual.events.Key","title":"Key","text":"
    Key(key, character)\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    The key that was pressed.

    required str | None

    A printable character or None if it is not printable.

    required"},{"location":"api/events/#textual.events.Key(key)","title":"key","text":""},{"location":"api/events/#textual.events.Key(character)","title":"character","text":""},{"location":"api/events/#textual.events.Key.aliases","title":"aliases instance-attribute","text":"
    aliases = _get_key_aliases(key)\n

    The aliases for the key, including the key itself.

    "},{"location":"api/events/#textual.events.Key.character","title":"character instance-attribute","text":"
    character = (\n    key\n    if len(key) == 1\n    else None if character is None else character\n)\n

    A printable character or None if it is not printable.

    "},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable\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.key","title":"key instance-attribute","text":"
    key = key\n

    The key that was pressed.

    "},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
    name\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\n

    The corresponding name for every alias in aliases list.

    "},{"location":"api/events/#textual.events.Leave","title":"Leave","text":"
    Leave(node)\n

    Bases: Event

    Sent when the mouse is moved away from a widget, or if a widget is programmatically disabled while hovered.

    Note that this widget bubbles, so a widget may receive Leave events for any child widgets. Check the node parameter for the original widget that was previously under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Leave.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was previously directly under the mouse.

    "},{"location":"api/events/#textual.events.Load","title":"Load","text":"
    Load()\n

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Mount","title":"Mount","text":"
    Mount()\n

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture","text":"
    MouseCapture(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:

    Name Type Description Default Offset

    The position of the mouse when captured.

    required"},{"location":"api/events/#textual.events.MouseCapture(mouse_position)","title":"mouse_position","text":""},{"location":"api/events/#textual.events.MouseCapture.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when captured.

    "},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown","text":"
    MouseDown(\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: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent","text":"
    MouseEvent(\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:

    Name Type Description Default int

    The relative x coordinate.

    required int

    The relative y coordinate.

    required int

    Change in x since the last message.

    required int

    Change in y since the last message.

    required int

    Indexed of the pressed button.

    required bool

    True if the shift key is pressed.

    required bool

    True if the meta key is pressed.

    required bool

    True if the ctrl key is pressed.

    required int | None

    The absolute x coordinate.

    None int | None

    The absolute y coordinate.

    None Style | None

    The Rich Style under the mouse cursor.

    None"},{"location":"api/events/#textual.events.MouseEvent(x)","title":"x","text":""},{"location":"api/events/#textual.events.MouseEvent(y)","title":"y","text":""},{"location":"api/events/#textual.events.MouseEvent(delta_x)","title":"delta_x","text":""},{"location":"api/events/#textual.events.MouseEvent(delta_y)","title":"delta_y","text":""},{"location":"api/events/#textual.events.MouseEvent(button)","title":"button","text":""},{"location":"api/events/#textual.events.MouseEvent(shift)","title":"shift","text":""},{"location":"api/events/#textual.events.MouseEvent(meta)","title":"meta","text":""},{"location":"api/events/#textual.events.MouseEvent(ctrl)","title":"ctrl","text":""},{"location":"api/events/#textual.events.MouseEvent(screen_x)","title":"screen_x","text":""},{"location":"api/events/#textual.events.MouseEvent(screen_y)","title":"screen_y","text":""},{"location":"api/events/#textual.events.MouseEvent(style)","title":"style","text":""},{"location":"api/events/#textual.events.MouseEvent.button","title":"button instance-attribute","text":"
    button = button\n

    Indexed of the pressed button.

    "},{"location":"api/events/#textual.events.MouseEvent.ctrl","title":"ctrl instance-attribute","text":"
    ctrl = ctrl\n

    True if the ctrl key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
    delta\n

    Mouse coordinate delta (change since last event).

    "},{"location":"api/events/#textual.events.MouseEvent.delta_x","title":"delta_x instance-attribute","text":"
    delta_x = delta_x\n

    Change in x since the last message.

    "},{"location":"api/events/#textual.events.MouseEvent.delta_y","title":"delta_y instance-attribute","text":"
    delta_y = delta_y\n

    Change in y since the last message.

    "},{"location":"api/events/#textual.events.MouseEvent.meta","title":"meta instance-attribute","text":"
    meta = meta\n

    True if the meta key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
    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\n

    Mouse coordinate relative to the screen.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_x","title":"screen_x instance-attribute","text":"
    screen_x = x if screen_x is None else screen_x\n

    The absolute x coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_y","title":"screen_y instance-attribute","text":"
    screen_y = y if screen_y is None else screen_y\n

    The absolute y coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.shift","title":"shift instance-attribute","text":"
    shift = shift\n

    True if the shift key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.style","title":"style property writable","text":"
    style\n

    The (Rich) Style under the cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.x","title":"x instance-attribute","text":"
    x = x\n

    The relative x coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.y","title":"y instance-attribute","text":"
    y = y\n

    The relative y coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset","text":"
    get_content_offset(widget)\n

    Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

    Parameters:

    Name Type Description Default Widget

    Widget receiving the event.

    required

    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(widget)","title":"widget","text":""},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture","text":"
    get_content_offset_capture(widget)\n

    Get offset from a widget's content area.

    This method works even if the offset is outside the widget content region.

    Parameters:

    Name Type Description Default Widget

    Widget receiving the event.

    required

    Returns:

    Type Description Offset

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture(widget)","title":"widget","text":""},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove","text":"
    MouseMove(\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: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease","text":"
    MouseRelease(mouse_position)\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Offset

    The position of the mouse when released.

    required"},{"location":"api/events/#textual.events.MouseRelease(mouse_position)","title":"mouse_position","text":""},{"location":"api/events/#textual.events.MouseRelease.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when released.

    "},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown","text":"
    MouseScrollDown(\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: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp","text":"
    MouseScrollUp(\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: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp","text":"
    MouseUp(\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: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Paste","title":"Paste","text":"
    Paste(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:

    Name Type Description Default str

    The text that has been pasted.

    required"},{"location":"api/events/#textual.events.Paste(text)","title":"text","text":""},{"location":"api/events/#textual.events.Paste.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was pasted.

    "},{"location":"api/events/#textual.events.Print","title":"Print","text":"
    Print(text, stderr=False)\n

    Bases: Event

    Sent to a widget that is capturing print.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    Text that was printed.

    required bool

    True if the print was to stderr, or False for stdout.

    False Note

    Python's print output can be captured with App.begin_capture_print.

    "},{"location":"api/events/#textual.events.Print(text)","title":"text","text":""},{"location":"api/events/#textual.events.Print(stderr)","title":"stderr","text":""},{"location":"api/events/#textual.events.Print.stderr","title":"stderr instance-attribute","text":"
    stderr = stderr\n

    True if the print was to stderr, or False for stdout.

    "},{"location":"api/events/#textual.events.Print.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was printed.

    "},{"location":"api/events/#textual.events.Ready","title":"Ready","text":"
    Ready()\n

    Bases: Event

    Sent to the App when the DOM is ready and the first frame has been displayed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Resize","title":"Resize","text":"
    Resize(size, virtual_size, container_size=None)\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Size

    The new size of the Widget.

    required Size

    The virtual size (scrollable size) of the Widget.

    required Size | None

    The size of the Widget's container widget.

    None"},{"location":"api/events/#textual.events.Resize(size)","title":"size","text":""},{"location":"api/events/#textual.events.Resize(virtual_size)","title":"virtual_size","text":""},{"location":"api/events/#textual.events.Resize(container_size)","title":"container_size","text":""},{"location":"api/events/#textual.events.Resize.container_size","title":"container_size instance-attribute","text":"
    container_size = (\n    size if container_size is None else container_size\n)\n

    The size of the Widget's container widget.

    "},{"location":"api/events/#textual.events.Resize.size","title":"size instance-attribute","text":"
    size = size\n

    The new size of the Widget.

    "},{"location":"api/events/#textual.events.Resize.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size = virtual_size\n

    The virtual size (scrollable size) of the Widget.

    "},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume","text":"
    ScreenResume()\n

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend","text":"
    ScreenSuspend()\n

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Show","title":"Show","text":"
    Show()\n

    Bases: Event

    Sent when a widget is first displayed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Timer","title":"Timer","text":"
    Timer(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","text":"
    Unmount()\n

    Bases: Event

    Sent when a widget is unmounted and may no longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/filter/","title":"textual.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","text":"
    ANSIToTruecolor(terminal_theme, enabled=True)\n

    Bases: LineFilter

    Convert ANSI colors to their truecolor equivalents.

    Parameters:

    Name Type Description Default TerminalTheme

    A rich terminal theme.

    required"},{"location":"api/filter/#textual.filter.ANSIToTruecolor(terminal_theme)","title":"terminal_theme","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
    truecolor_style(style)\n

    Replace system colors with truecolor equivalent.

    Parameters:

    Name Type Description Default Style

    Style to apply truecolor filter to.

    required

    Returns:

    Type Description Style

    New style.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style(style)","title":"style","text":""},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter","text":"
    DimFilter(dim_factor=0.5)\n

    Bases: LineFilter

    Replace dim attributes with modified colors.

    Parameters:

    Name Type Description Default float

    The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

    0.5"},{"location":"api/filter/#textual.filter.DimFilter(dim_factor)","title":"dim_factor","text":""},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.DimFilter.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.DimFilter.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter","text":"
    LineFilter(enabled=True)\n

    Bases: ABC

    Base class for a line filter.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.LineFilter.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome","text":"
    Monochrome(enabled=True)\n

    Bases: LineFilter

    Convert all colors to monochrome.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.Monochrome.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.NoColor","title":"NoColor","text":"
    NoColor(enabled=True)\n

    Bases: LineFilter

    Remove all color information from segments.

    "},{"location":"api/filter/#textual.filter.NoColor.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.NoColor.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.NoColor.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
    dim_color(background, color, factor)\n

    Dim a color by blending towards the background

    Parameters:

    Name Type Description Default Color

    background color.

    required Color

    Foreground color.

    required float

    Blend factor

    required

    Returns:

    Type Description Color

    New dimmer color.

    "},{"location":"api/filter/#textual.filter.dim_color(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.dim_color(color)","title":"color","text":""},{"location":"api/filter/#textual.filter.dim_color(factor)","title":"factor","text":""},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
    dim_style(style, background, factor)\n

    Replace dim attribute with a dim color.

    Parameters:

    Name Type Description Default Style

    Style to dim.

    required float

    Blend factor.

    required

    Returns:

    Type Description Style

    New dimmed style.

    "},{"location":"api/filter/#textual.filter.dim_style(style)","title":"style","text":""},{"location":"api/filter/#textual.filter.dim_style(factor)","title":"factor","text":""},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
    monochrome_style(style)\n

    Convert colors in a style to monochrome.

    Parameters:

    Name Type Description Default Style

    A Rich Style.

    required

    Returns:

    Type Description Style

    A new Rich style.

    "},{"location":"api/filter/#textual.filter.monochrome_style(style)","title":"style","text":""},{"location":"api/fuzzy_matcher/","title":"textual.fuzzy","text":"

    Fuzzy matcher.

    This class is used by the command palette to match search terms.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher","text":"
    Matcher(query, *, match_style=None, case_sensitive=False)\n

    A fuzzy matcher.

    Parameters:

    Name Type Description Default str

    A query as typed in by the user.

    required Style | None

    The style to use to highlight matched portions of a string.

    None bool

    Should matching be case sensitive?

    False"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(query)","title":"query","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(match_style)","title":"match_style","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive\n

    Is this matcher case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_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\n

    The query string to look for.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern\n

    The regular expression pattern built from the query.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight","text":"
    highlight(candidate)\n

    Highlight the candidate with the fuzzy match.

    Parameters:

    Name Type Description Default str

    The candidate string to match against the query.

    required

    Returns:

    Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight(candidate)","title":"candidate","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match","text":"
    match(candidate)\n

    Match the candidate against the query.

    Parameters:

    Name Type Description Default str

    Candidate string to match against the query.

    required

    Returns:

    Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match(candidate)","title":"candidate","text":""},{"location":"api/geometry/","title":"textual.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 = 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 = 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 = 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 = Spacing(0, 0, 0, 0)\n

    A Spacing constant for no space.

    "},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
    SpacingDimensions = 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","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\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\n

    Is the offset at (0, 0)?

    "},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x class-attribute instance-attribute","text":"
    x = 0\n

    Offset in the x-axis (horizontal)

    "},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y class-attribute instance-attribute","text":"
    y = 0\n

    Offset in the y-axis (vertical)

    "},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend","text":"
    blend(destination, factor)\n

    Calculate a new offset on a line between this offset and a destination offset.

    Parameters:

    Name Type Description Default Offset

    Point where factor would be 1.0.

    required float

    A value between 0 and 1.0.

    required

    Returns:

    Type Description Offset

    A new point on a line between self and destination.

    "},{"location":"api/geometry/#textual.geometry.Offset.blend(destination)","title":"destination","text":""},{"location":"api/geometry/#textual.geometry.Offset.blend(factor)","title":"factor","text":""},{"location":"api/geometry/#textual.geometry.Offset.clamp","title":"clamp","text":"
    clamp(width, height)\n

    Clamp the offset to fit within a rectangle of width x height.

    Parameters:

    Name Type Description Default int

    Width to clamp.

    required int

    Height to clamp.

    required

    Returns:

    Type Description Offset

    A new offset.

    "},{"location":"api/geometry/#textual.geometry.Offset.clamp(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Offset.clamp(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to","text":"
    get_distance_to(other)\n

    Get the distance to another offset.

    Parameters:

    Name Type Description Default Offset

    An offset.

    required

    Returns:

    Type Description float

    Distance to other offset.

    "},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region","title":"Region","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\n

    The area under the region.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
    bottom\n

    Maximum Y value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
    bottom_left\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\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\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\n

    A range object for X coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
    column_span\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\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 = 0\n

    The height of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
    line_range\n

    A range object for Y coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
    line_span\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\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\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\n

    Maximum X value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
    size\n

    Get the size of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
    top_right\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 = 0\n

    The width of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.x","title":"x class-attribute instance-attribute","text":"
    x = 0\n

    Offset in the x-axis (horizontal).

    "},{"location":"api/geometry/#textual.geometry.Region.y","title":"y class-attribute instance-attribute","text":"
    y = 0\n

    Offset in the y-axis (vertical).

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset","text":"
    at_offset(offset)\n

    Get a new Region with the same size at a given offset.

    Parameters:

    Name Type Description Default tuple[int, int]

    An offset.

    required

    Returns:

    Type Description Region

    New Region with adjusted offset.

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip","text":"
    clip(width, height)\n

    Clip this region to fit within width, height.

    Parameters:

    Name Type Description Default int

    Width of bounds.

    required int

    Height of bounds.

    required

    Returns:

    Type Description Region

    Clipped region.

    "},{"location":"api/geometry/#textual.geometry.Region.clip(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Region.clip(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains","text":"
    contains(x, y)\n

    Check if a point is in the region.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains(x)","title":"x","text":""},{"location":"api/geometry/#textual.geometry.Region.contains(y)","title":"y","text":""},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point","text":"
    contains_point(point)\n

    Check if a point is in the region.

    Parameters:

    Name Type Description Default tuple[int, int]

    A tuple of x and y coordinates.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_point(point)","title":"point","text":""},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
    contains_region(other)\n

    Check if a region is entirely contained within this region.

    Parameters:

    Name Type Description Default Region

    A region.

    required

    Returns:

    Type Description bool

    True if the other region fits perfectly within this region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_region(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size","text":"
    crop_size(size)\n

    Get a region with the same offset, with a size no larger than size.

    Parameters:

    Name Type Description Default tuple[int, int]

    Maximum width and height (WIDTH, HEIGHT).

    required

    Returns:

    Type Description Region

    New region that could fit within size.

    "},{"location":"api/geometry/#textual.geometry.Region.crop_size(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand","text":"
    expand(size)\n

    Increase the size of the region by adding a border.

    Parameters:

    Name Type Description Default tuple[int, int]

    Additional width and height.

    required

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.expand(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
    from_corners(x1, y1, x2, y2)\n

    Construct a Region form the top left and bottom right corners.

    Parameters:

    Name Type Description Default int

    Top left x.

    required int

    Top left y.

    required int

    Bottom right x.

    required int

    Bottom right y.

    required

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_corners(x1)","title":"x1","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(y1)","title":"y1","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(x2)","title":"x2","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(y2)","title":"y2","text":""},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
    from_offset(offset, size)\n

    Create a region from offset and size.

    Parameters:

    Name Type Description Default tuple[int, int]

    Offset (top left point).

    required tuple[int, int]

    Dimensions of region.

    required

    Returns:

    Type Description Region

    A region instance.

    "},{"location":"api/geometry/#textual.geometry.Region.from_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.from_offset(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
    from_union(regions)\n

    Create a Region from the union of other regions.

    Parameters:

    Name Type Description Default Collection[Region]

    One or more regions.

    required

    Returns:

    Type Description Region

    A Region that encloses all other regions.

    "},{"location":"api/geometry/#textual.geometry.Region.from_union(regions)","title":"regions","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
    get_scroll_to_visible(window_region, region, *, top=False)\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:

    Name Type Description Default Region

    The window region.

    required Region

    The region to move inside the window.

    required bool

    Get offset to top of window.

    False

    Returns:

    Type Description Offset

    An offset required to add to region to move it inside window_region.

    "},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(window_region)","title":"window_region","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(top)","title":"top","text":""},{"location":"api/geometry/#textual.geometry.Region.get_spacing_between","title":"get_spacing_between","text":"
    get_spacing_between(region)\n

    Get spacing between two regions.

    Parameters:

    Name Type Description Default Region

    Another region.

    required

    Returns:

    Type Description Spacing

    Spacing that if subtracted from self produces region.

    "},{"location":"api/geometry/#textual.geometry.Region.get_spacing_between(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
    grow(margin)\n

    Grow a region by adding spacing.

    Parameters:

    Name Type Description Default tuple[int, int, int, int]

    Grow space by (<top>, <right>, <bottom>, <left>).

    required

    Returns:

    Type Description Region

    New region.

    "},{"location":"api/geometry/#textual.geometry.Region.grow(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect","text":"
    inflect(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:

    Name Type Description Default int

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    +1 int

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    +1 Spacing | None

    Additional margin.

    None

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.inflect(x_axis)","title":"x_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect(y_axis)","title":"y_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
    intersection(region)\n

    Get the overlapping portion of the two regions.

    Parameters:

    Name Type Description Default Region

    A region that overlaps this region.

    required

    Returns:

    Type Description Region

    A new region that covers when the two regions overlap.

    "},{"location":"api/geometry/#textual.geometry.Region.intersection(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
    overlaps(other)\n

    Check if another region overlaps this region.

    Parameters:

    Name Type Description Default Region

    A Region.

    required

    Returns:

    Type Description bool

    True if other region shares any cells with this region.

    "},{"location":"api/geometry/#textual.geometry.Region.overlaps(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
    shrink(margin)\n

    Shrink a region by subtracting spacing.

    Parameters:

    Name Type Description Default tuple[int, int, int, int]

    Shrink space by (<top>, <right>, <bottom>, <left>).

    required

    Returns:

    Type Description Region

    The new, smaller region.

    "},{"location":"api/geometry/#textual.geometry.Region.shrink(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
    split(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:

    Name Type Description Default int

    Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

    required int

    Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

    required

    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(cut_x)","title":"cut_x","text":""},{"location":"api/geometry/#textual.geometry.Region.split(cut_y)","title":"cut_y","text":""},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
    split_horizontal(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:

    Name Type Description Default int

    An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

    required

    Returns:

    Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_horizontal(cut)","title":"cut","text":""},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
    split_vertical(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:

    Name Type Description Default int

    An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

    required

    Returns:

    Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_vertical(cut)","title":"cut","text":""},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
    translate(offset)\n

    Move the offset of the Region.

    Parameters:

    Name Type Description Default tuple[int, int]

    Offset to add to region.

    required

    Returns:

    Type Description Region

    A new region shifted by (x, y).

    "},{"location":"api/geometry/#textual.geometry.Region.translate(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside","text":"
    translate_inside(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:

    Name Type Description Default Region

    A container region.

    required bool

    Allow translation of X axis.

    True bool

    Allow translation of Y axis.

    True

    Returns:

    Type Description Region

    A new region with same dimensions that fits with inside container.

    "},{"location":"api/geometry/#textual.geometry.Region.translate_inside(container)","title":"container","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside(x_axis)","title":"x_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside(y_axis)","title":"y_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
    union(region)\n

    Get the smallest region that contains both regions.

    Parameters:

    Name Type Description Default Region

    Another region.

    required

    Returns:

    Type Description Region

    An optimally sized region to cover both regions.

    "},{"location":"api/geometry/#textual.geometry.Region.union(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Size","title":"Size","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\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 = 0\n

    The height in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
    line_range\n

    A range object that covers values between 0 and height.

    "},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
    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 = 0\n

    The width in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.clamp_offset","title":"clamp_offset","text":"
    clamp_offset(offset)\n

    Clamp an offset to fit within the width x height.

    Parameters:

    Name Type Description Default Offset

    An offset.

    required

    Returns:

    Type Description Offset

    A new offset that will fit inside the dimensions defined in the Size.

    "},{"location":"api/geometry/#textual.geometry.Size.clamp_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains","text":"
    contains(x, y)\n

    Check if a point is in area defined by the size.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains(x)","title":"x","text":""},{"location":"api/geometry/#textual.geometry.Size.contains(y)","title":"y","text":""},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point","text":"
    contains_point(point)\n

    Check if a point is in the area defined by the size.

    Parameters:

    Name Type Description Default tuple[int, int]

    A tuple of x and y coordinates.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains_point(point)","title":"point","text":""},{"location":"api/geometry/#textual.geometry.Size.with_height","title":"with_height","text":"
    with_height(height)\n

    Get a new Size with just the height changed.

    Parameters:

    Name Type Description Default int

    New height.

    required

    Returns:

    Type Description Size

    New Size instance.

    "},{"location":"api/geometry/#textual.geometry.Size.with_height(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Size.with_width","title":"with_width","text":"
    with_width(width)\n

    Get a new Size with just the width changed.

    Parameters:

    Name Type Description Default int

    New width.

    required

    Returns:

    Type Description Size

    New Size instance.

    "},{"location":"api/geometry/#textual.geometry.Size.with_width(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing","text":"

    Bases: NamedTuple

    Stores spacing around a widget, 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 = 0\n

    Space from the bottom of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
    bottom_right\n

    A pair of integers for the right, and bottom space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
    css\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\n

    Total space in the y axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left class-attribute instance-attribute","text":"
    left = 0\n

    Space from the left of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right class-attribute instance-attribute","text":"
    right = 0\n

    Space from the right of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top class-attribute instance-attribute","text":"
    top = 0\n

    Space from the top of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
    top_left\n

    A pair of integers for the left, and top space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
    totals\n

    A pair of integers for the total horizontal and vertical space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
    width\n

    Total space in the x axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
    all(amount)\n

    Construct a Spacing with a given amount of spacing on all edges.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to all edges.

    required

    Returns:

    Type Description Spacing

    Spacing(amount, amount, amount, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.all(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum","text":"
    grow_maximum(other)\n

    Grow spacing with a maximum.

    Parameters:

    Name Type Description Default Spacing

    Spacing object.

    required

    Returns:

    Type Description Spacing

    New spacing where the values are maximum of the two values.

    "},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
    horizontal(amount)\n

    Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to horizontal edges.

    required

    Returns:

    Type Description Spacing

    Spacing(0, amount, 0, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.horizontal(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
    unpack(pad)\n

    Unpack padding specified in CSS style.

    Parameters:

    Name Type Description Default SpacingDimensions

    An integer, or tuple of 1, 2, or 4 integers.

    required

    Raises:

    Type Description ValueError

    If pad is an invalid value.

    Returns:

    Type Description Spacing

    New Spacing object.

    "},{"location":"api/geometry/#textual.geometry.Spacing.unpack(pad)","title":"pad","text":""},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
    vertical(amount)\n

    Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to vertical edges.

    required

    Returns:

    Type Description Spacing

    Spacing(amount, 0, amount, 0)

    "},{"location":"api/geometry/#textual.geometry.Spacing.vertical(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp","text":"
    clamp(value, minimum, maximum)\n

    Restrict a value to a given range.

    If value is less than the minimum, return the minimum. If value is greater than the maximum, return the maximum. Otherwise, return value.

    The minimum and maximum arguments values may be given in reverse order.

    Parameters:

    Name Type Description Default T

    A value.

    required T

    Minimum value.

    required T

    Maximum value.

    required

    Returns:

    Type Description T

    New value that is not less than the minimum or greater than the maximum.

    "},{"location":"api/geometry/#textual.geometry.clamp(value)","title":"value","text":""},{"location":"api/geometry/#textual.geometry.clamp(minimum)","title":"minimum","text":""},{"location":"api/geometry/#textual.geometry.clamp(maximum)","title":"maximum","text":""},{"location":"api/lazy/","title":"textual.lazy","text":"

    Tools for lazy loading widgets.

    "},{"location":"api/lazy/#textual.lazy.Lazy","title":"Lazy","text":"
    Lazy(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:

    Name Type Description Default Widget

    A widget that should be mounted after a refresh.

    required"},{"location":"api/lazy/#textual.lazy.Lazy(widget)","title":"widget","text":""},{"location":"api/logger/","title":"textual","text":"

    The root Textual module.

    Exposes some commonly used symbols.

    "},{"location":"api/logger/#textual.log","title":"log module-attribute","text":"
    log = Logger(None)\n

    Global logger that logs to the currently active app.

    Example
    from textual import log\nlog(locals())\n
    "},{"location":"api/logger/#textual.Logger","title":"Logger","text":"
    Logger(log_callable, group=INFO, verbosity=NORMAL)\n

    A logger class that logs to the Textual console.

    "},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
    debug\n

    Logs debug messages.

    "},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
    error\n

    Logs errors.

    "},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
    event\n

    Logs events.

    "},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
    info\n

    Logs information.

    "},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
    logging\n

    Logs from stdlib logging module.

    "},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
    system\n

    Logs system information.

    "},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
    verbose\n

    A verbose logger.

    "},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
    warning\n

    Logs warnings.

    "},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
    worker\n

    Logs worker information.

    "},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity","text":"
    verbosity(verbose)\n

    Get a new logger with selective verbosity.

    Parameters:

    Name Type Description Default bool

    True to use HIGH verbosity, otherwise NORMAL.

    required

    Returns:

    Type Description Logger

    New logger.

    "},{"location":"api/logger/#textual.Logger.verbosity(verbose)","title":"verbose","text":""},{"location":"api/logger/#textual.LoggerError","title":"LoggerError","text":"

    Bases: Exception

    Raised when the logger failed.

    "},{"location":"api/logger/#textual.on","title":"on","text":"
    on(message_type, selector=None, **kwargs)\n

    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\", pane=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n

    Parameters:

    Name Type Description Default type[Message]

    The message type (i.e. the class).

    required str | None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    None str

    Additional selectors for other attributes of the message.

    {}"},{"location":"api/logger/#textual.on(message_type)","title":"message_type","text":""},{"location":"api/logger/#textual.on(selector)","title":"selector","text":""},{"location":"api/logger/#textual.on(**kwargs)","title":"**kwargs","text":""},{"location":"api/logger/#textual.work","title":"work","text":"
    work(\n    method: Callable[\n        FactoryParamSpec, Coroutine[None, None, ReturnType]\n    ],\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Callable[FactoryParamSpec, \"Worker[ReturnType]\"]\n
    work(\n    method: Callable[FactoryParamSpec, ReturnType],\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Callable[FactoryParamSpec, \"Worker[ReturnType]\"]\n
    work(\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Decorator[..., ReturnType]\n
    work(\n    method=None,\n    *,\n    name=\"\",\n    group=\"default\",\n    exit_on_error=True,\n    exclusive=False,\n    description=None,\n    thread=False\n)\n

    A decorator used to create workers.

    Parameters:

    Name Type Description Default Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

    A function or coroutine.

    None str

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Cancel all workers in the same group.

    False str | None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    None bool

    Mark the method as a thread worker.

    False"},{"location":"api/logger/#textual.work(method)","title":"method","text":""},{"location":"api/logger/#textual.work(name)","title":"name","text":""},{"location":"api/logger/#textual.work(group)","title":"group","text":""},{"location":"api/logger/#textual.work(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/logger/#textual.work(exclusive)","title":"exclusive","text":""},{"location":"api/logger/#textual.work(description)","title":"description","text":""},{"location":"api/logger/#textual.work(thread)","title":"thread","text":""},{"location":"api/logging/","title":"textual.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","text":"
    TextualHandler(stderr=True, stdout=False)\n

    Bases: Handler

    A Logging handler for Textual apps.

    Parameters:

    Name Type Description Default bool

    Log to stderr when there is no active app.

    True bool

    Log to stdout when there is no active app.

    False"},{"location":"api/logging/#textual.logging.TextualHandler(stderr)","title":"stderr","text":""},{"location":"api/logging/#textual.logging.TextualHandler(stdout)","title":"stdout","text":""},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit","text":"
    emit(record)\n

    Invoked by logging.

    "},{"location":"api/map_geometry/","title":"textual.map_geometry","text":"

    A data structure returned by screen.find_widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry","title":"MapGeometry","text":"

    Bases: NamedTuple

    Defines the absolute location of a Widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.clip","title":"clip instance-attribute","text":"
    clip\n

    A region to clip the widget by (if a Widget is within a container).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.container_size","title":"container_size instance-attribute","text":"
    container_size\n

    The container size (area not occupied by scrollbars).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
    dock_gutter\n

    Space from the container reserved by docked widgets.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.order","title":"order instance-attribute","text":"
    order\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.map_geometry.MapGeometry.region","title":"region instance-attribute","text":"
    region\n

    The (screen) region occupied by the widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
    virtual_region\n

    The region relative to the container (but not necessarily visible).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size\n

    The virtual size (scrollable area) of a widget if it is a container.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.visible_region","title":"visible_region property","text":"
    visible_region\n

    The Widget region after clipping.

    "},{"location":"api/message/","title":"textual.message","text":"

    The base class for all messages (including events).

    "},{"location":"api/message/#textual.message.Message","title":"Message","text":"
    Message()\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()\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\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\n

    Name of the default message handler.

    "},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
    is_forwarded\n

    Has the message been forwarded?

    "},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default","text":"
    prevent_default(prevent=True)\n

    Suppress the default action(s). This will prevent handlers in any base classes from being called.

    Parameters:

    Name Type Description Default bool

    True if the default action should be suppressed, or False if the default actions should be performed.

    True"},{"location":"api/message/#textual.message.Message.prevent_default(prevent)","title":"prevent","text":""},{"location":"api/message/#textual.message.Message.set_sender","title":"set_sender","text":"
    set_sender(sender)\n

    Set the sender of the message.

    Parameters:

    Name Type Description Default MessagePump

    The sender.

    required Note

    When creating a message the sender is automatically set. Normally there will be no need for this method to be called. This method will be used when strict control is required over the sender of a message.

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/message/#textual.message.Message.set_sender(sender)","title":"sender","text":""},{"location":"api/message/#textual.message.Message.stop","title":"stop","text":"
    stop(stop=True)\n

    Stop propagation of the message to parent.

    Parameters:

    Name Type Description Default bool

    The stop flag.

    True"},{"location":"api/message/#textual.message.Message.stop(stop)","title":"stop","text":""},{"location":"api/message_pump/","title":"textual.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","text":"
    MessagePump(parent=None)\n

    Base class which supplies a message pump.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
    app\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\n

    Does this object have a parent?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
    is_attached\n

    Is this node linked to the app through the DOM?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_dom_root","title":"is_dom_root property","text":"
    is_dom_root\n

    Is this a root node (i.e. the App)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
    is_parent_active\n

    Is the parent active?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
    is_running\n

    Is the message pump running (potentially processing messages)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
    log\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\n

    The current size of the message queue.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.message_signal","title":"message_signal instance-attribute","text":"
    message_signal = Signal(self, 'messages')\n

    Subscribe to this signal to be notified of all messages sent to this widget.

    This is a fairly low-level mechanism, and shouldn't replace regular message handling.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh","text":"
    call_after_refresh(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:

    Name Type Description Default Callback

    A callable.

    required

    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_after_refresh(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later","text":"
    call_later(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:

    Name Type Description Default Callback

    Callable to call next.

    required Any

    Positional arguments to pass to the callable.

    () 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_later(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later(*args)","title":"*args","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later(**kwargs)","title":"**kwargs","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next","text":"
    call_next(callback, *args, **kwargs)\n

    Schedule a callback to run immediately after processing the current message.

    Parameters:

    Name Type Description Default Callback

    Callable to run after current event.

    required Any

    Positional arguments to pass to the callable.

    () Any

    Keyword arguments to pass to the callable.

    {}"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(*args)","title":"*args","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(**kwargs)","title":"**kwargs","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle","text":"
    check_idle()\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","text":"
    check_message_enabled(message)\n

    Check if a given message is enabled (allowed to be sent).

    Parameters:

    Name Type Description Default Message

    A message object.

    required

    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.check_message_enabled(message)","title":"message","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages","text":"
    disable_messages(*messages)\n

    Disable message types from being processed.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages","text":"
    enable_messages(*messages)\n

    Enable processing of messages types.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
    on_event(event)\n

    Called to process an event.

    Parameters:

    Name Type Description Default Event

    An Event object.

    required"},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event(event)","title":"event","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message","text":"
    post_message(message)\n

    Posts a message on to this widget's queue.

    Parameters:

    Name Type Description Default Message

    A message (including Event).

    required

    Returns:

    Type Description bool

    True if the messages was processed, False if it wasn't.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message(message)","title":"message","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent","text":"
    prevent(*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","text":"
    set_interval(\n    interval,\n    callback=None,\n    *,\n    name=None,\n    repeat=0,\n    pause=False\n)\n

    Call a function at periodic intervals.

    Parameters:

    Name Type Description Default float

    Time (in seconds) between calls.

    required TimerCallback | None

    Function to call.

    None str | None

    Name of the timer object.

    None int

    Number of times to repeat the call or 0 for continuous.

    0 bool

    Start the timer paused.

    False

    Returns:

    Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(interval)","title":"interval","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(name)","title":"name","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(repeat)","title":"repeat","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(pause)","title":"pause","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer","text":"
    set_timer(delay, callback=None, *, name=None, pause=False)\n

    Call a function after a delay.

    Example
    def ready():\n    self.notify(\"Your soft boiled egg is ready!\")\n# Call ready() after 3 minutes\nself.set_timer(3 * 60, ready)\n

    Parameters:

    Name Type Description Default float

    Time (in seconds) to wait before invoking callback.

    required TimerCallback | None

    Callback to call after time has expired.

    None str | None

    Name of the timer (for debug).

    None bool

    Start timer paused.

    False

    Returns:

    Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(delay)","title":"delay","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(name)","title":"name","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(pause)","title":"pause","text":""},{"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\", pane=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n

    Parameters:

    Name Type Description Default type[Message]

    The message type (i.e. the class).

    required str | None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    None str

    Additional selectors for other attributes of the message.

    {}"},{"location":"api/on/#textual.on(message_type)","title":"message_type","text":""},{"location":"api/on/#textual.on(selector)","title":"selector","text":""},{"location":"api/on/#textual.on(**kwargs)","title":"**kwargs","text":""},{"location":"api/pilot/","title":"textual.pilot","text":"

    This module contains the Pilot class 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","text":"

    Bases: Exception

    Raised when the pilot mouse target is outside of the (visible) screen.

    "},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot","text":"
    Pilot(app)\n

    Bases: Generic[ReturnType]

    Pilot object to drive an app.

    "},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
    app\n
    "},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
    click(\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:

    Name Type Description Default type[Widget] | str | 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.

    None tuple[int, int]

    The offset to click. The offset is relative to the selector provided or to the screen, if no selector is provided.

    (0, 0) bool

    Click with the shift key held down.

    False bool

    Click with the meta key held down.

    False bool

    Click with the control key held down.

    False

    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.click(selector)","title":"selector","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
    exit(result)\n

    Exit the app with the given result.

    Parameters:

    Name Type Description Default ReturnType

    The app result returned by run or run_async.

    required"},{"location":"api/pilot/#textual.pilot.Pilot.exit(result)","title":"result","text":""},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
    hover(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:

    Name Type Description Default type[Widget] | str | 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.

    None tuple[int, int]

    The offset to hover. The offset is relative to the selector provided or to the screen, if no selector is provided.

    (0, 0)

    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.hover(selector)","title":"selector","text":""},{"location":"api/pilot/#textual.pilot.Pilot.hover(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down","title":"mouse_down async","text":"
    mouse_down(\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:

    Name Type Description Default type[Widget] | str | 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.

    None tuple[int, int]

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    (0, 0) bool

    Simulate the event with the shift key held down.

    False bool

    Simulate the event with the meta key held down.

    False bool

    Simulate the event with the control key held down.

    False

    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_down(selector)","title":"selector","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up","title":"mouse_up async","text":"
    mouse_up(\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:

    Name Type Description Default type[Widget] | str | 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.

    None tuple[int, int]

    The offset for the event. The offset is relative to the selector provided or to the screen, if no selector is provided.

    (0, 0) bool

    Simulate the event with the shift key held down.

    False bool

    Simulate the event with the meta key held down.

    False bool

    Simulate the event with the control key held down.

    False

    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(selector)","title":"selector","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
    pause(delay=None)\n

    Insert a pause.

    Parameters:

    Name Type Description Default float | None

    Seconds to pause, or None to wait for cpu idle.

    None"},{"location":"api/pilot/#textual.pilot.Pilot.pause(delay)","title":"delay","text":""},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
    press(*keys)\n

    Simulate key-presses.

    Parameters:

    Name Type Description Default str

    Keys to press.

    ()"},{"location":"api/pilot/#textual.pilot.Pilot.press(*keys)","title":"*keys","text":""},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal","title":"resize_terminal async","text":"
    resize_terminal(width, height)\n

    Resize the terminal to the given dimensions.

    Parameters:

    Name Type Description Default int

    The new width of the terminal.

    required int

    The new height of the terminal.

    required"},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal(width)","title":"width","text":""},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal(height)","title":"height","text":""},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
    wait_for_animation()\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":"
    wait_for_scheduled_animations()\n

    Wait for any current and scheduled animations to complete.

    "},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout","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":"textual.css.query","text":"

    This module contains the DOMQuery class and related objects.

    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","text":"
    DOMQuery(\n    node,\n    *,\n    filter=None,\n    exclude=None,\n    deep=True,\n    parent=None\n)\n

    Bases: Generic[QueryType]

    Warning

    You won't need to construct this manually, as DOMQuery objects are returned by query.

    Parameters:

    Name Type Description Default DOMNode

    A DOM node.

    required str | None

    Query to filter children in the node.

    None str | None

    Query to exclude children in the node.

    None bool

    Query should be deep, i.e. recursive.

    True DOMQuery | None

    The parent query, if this is the result of filtering another query.

    None

    Raises:

    Type Description InvalidQueryFormat

    If the format of the query is invalid.

    "},{"location":"api/query/#textual.css.query.DOMQuery(node)","title":"node","text":""},{"location":"api/query/#textual.css.query.DOMQuery(filter)","title":"filter","text":""},{"location":"api/query/#textual.css.query.DOMQuery(exclude)","title":"exclude","text":""},{"location":"api/query/#textual.css.query.DOMQuery(deep)","title":"deep","text":""},{"location":"api/query/#textual.css.query.DOMQuery(parent)","title":"parent","text":""},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
    node\n

    The node being queried.

    "},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
    nodes\n

    Lazily evaluate nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class","text":"
    add_class(*class_names)\n

    Add the given class name(s) to nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.blur","title":"blur","text":"
    blur()\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","text":"
    exclude(selector)\n

    Exclude nodes that match a given selector.

    Parameters:

    Name Type Description Default str

    A CSS selector.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    New DOM query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.exclude(selector)","title":"selector","text":""},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter","text":"
    filter(selector)\n

    Filter this set by the given CSS selector.

    Parameters:

    Name Type Description Default str

    A CSS selector.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    New DOM Query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.filter(selector)","title":"selector","text":""},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first","text":"
    first() -> QueryType\n
    first(expect_type: type[ExpectType]) -> ExpectType\n
    first(expect_type=None)\n

    Get the first matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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.first(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.focus","title":"focus","text":"
    focus()\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","text":"
    last() -> QueryType\n
    last(expect_type: type[ExpectType]) -> ExpectType\n
    last(expect_type=None)\n

    Get the last matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one","text":"
    only_one() -> QueryType\n
    only_one(expect_type: type[ExpectType]) -> ExpectType\n
    only_one(expect_type=None)\n

    Get the only matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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.only_one(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh","text":"
    refresh(*, repaint=True, layout=False, recompose=False)\n

    Refresh matched nodes.

    Parameters:

    Name Type Description Default bool

    Repaint node(s).

    True bool

    Layout node(s).

    False bool

    Recompose node(s).

    False

    Returns:

    Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.refresh(repaint)","title":"repaint","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh(layout)","title":"layout","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh(recompose)","title":"recompose","text":""},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove","text":"
    remove()\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","text":"
    remove_class(*class_names)\n

    Remove the given class names from the nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results","text":"
    results() -> Iterator[QueryType]\n
    results(\n    filter_type: type[ExpectType],\n) -> Iterator[ExpectType]\n
    results(filter_type=None)\n

    Get query results, optionally filtered by a given type.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    A Widget class to filter results, or None for no filter.

    None

    Yields:

    Type Description QueryType | ExpectType

    Iterator[Widget | ExpectType]: An iterator of Widget instances.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results(filter_type)","title":"filter_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set","title":"set","text":"
    set(\n    display=None, visible=None, disabled=None, loading=None\n)\n

    Sets common attributes on matched nodes.

    Parameters:

    Name Type Description Default bool | None

    Set display attribute on nodes, or None for no change.

    None bool | None

    Set visible attribute on nodes, or None for no change.

    None bool | None

    Set disabled attribute on nodes, or None for no change.

    None bool | None

    Set loading attribute on nodes, or None for no change.

    None

    Returns:

    Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set(display)","title":"display","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(visible)","title":"visible","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(disabled)","title":"disabled","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(loading)","title":"loading","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class","text":"
    set_class(add, *class_names)\n

    Set the given class name(s) according to a condition.

    Parameters:

    Name Type Description Default bool

    Add the classes if True, otherwise remove them.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_class(add)","title":"add","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes","text":"
    set_classes(classes)\n

    Set the classes on nodes to exactly the given set.

    Parameters:

    Name Type Description Default str | Iterable[str]

    A string of space separated classes, or an iterable of class names.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_classes(classes)","title":"classes","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles","text":"
    set_styles(css=None, **update_styles)\n

    Set styles on matched nodes.

    Parameters:

    Name Type Description Default str | None

    CSS declarations to parser, or None.

    None"},{"location":"api/query/#textual.css.query.DOMQuery.set_styles(css)","title":"css","text":""},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class","text":"
    toggle_class(*class_names)\n

    Toggle the given class names from matched nodes.

    "},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat","text":"

    Bases: QueryError

    Query did not parse correctly.

    "},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches","text":"

    Bases: QueryError

    No nodes matched the query.

    "},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError","text":"

    Bases: Exception

    Base class for a query related error.

    "},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches","text":"

    Bases: QueryError

    Too many nodes matched the query.

    "},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType","text":"

    Bases: QueryError

    Query result was not of the correct type.

    "},{"location":"api/reactive/","title":"textual.reactive","text":"

    This module contains the Reactive class which implements reactivity.

    "},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive","text":"
    Reactive(\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=False,\n    always_update=False,\n    compute=True,\n    recompose=False,\n    bindings=False\n)\n

    Bases: Generic[ReactiveType]

    Reactive descriptor.

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Perform a layout on change.

    False bool

    Perform a repaint on change.

    True bool

    Call watchers on initialize (post mount).

    False bool

    Call watchers even when the new value equals the old value.

    False bool

    Run compute methods when attribute is changed.

    True bool

    Compose the widget again when the attribute changes.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.Reactive(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.Reactive(layout)","title":"layout","text":""},{"location":"api/reactive/#textual.reactive.Reactive(repaint)","title":"repaint","text":""},{"location":"api/reactive/#textual.reactive.Reactive(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.Reactive(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.Reactive(compute)","title":"compute","text":""},{"location":"api/reactive/#textual.reactive.Reactive(recompose)","title":"recompose","text":""},{"location":"api/reactive/#textual.reactive.Reactive(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.Reactive.owner","title":"owner property","text":"
    owner\n

    The owner (class) where the reactive was declared.

    "},{"location":"api/reactive/#textual.reactive.ReactiveError","title":"ReactiveError","text":"

    Bases: Exception

    Base class for reactive errors.

    "},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError","text":"

    Bases: ReactiveError

    Raised when an attribute has public and private compute methods.

    "},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive","text":"
    reactive(\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=True,\n    always_update=False,\n    recompose=False,\n    bindings=False\n)\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute.

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Perform a layout on change.

    False bool

    Perform a repaint on change.

    True bool

    Call watchers on initialize (post mount).

    True bool

    Call watchers even when the new value equals the old value.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.reactive(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.reactive(layout)","title":"layout","text":""},{"location":"api/reactive/#textual.reactive.reactive(repaint)","title":"repaint","text":""},{"location":"api/reactive/#textual.reactive.reactive(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.reactive(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.reactive(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.var","title":"var","text":"
    var(\n    default, init=True, always_update=False, bindings=False\n)\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute (with no auto-refresh).

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Call watchers on initialize (post mount).

    True bool

    Call watchers even when the new value equals the old value.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.var(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.var(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.var(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.var(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.await_watcher","title":"await_watcher async","text":"
    await_watcher(obj, awaitable)\n

    Coroutine to await an awaitable returned from a watcher

    "},{"location":"api/reactive/#textual.reactive.invoke_watcher","title":"invoke_watcher","text":"
    invoke_watcher(\n    watcher_object, watch_function, old_value, value\n)\n

    Invoke a watch function.

    Parameters:

    Name Type Description Default Reactable

    The object watching for the changes.

    required WatchCallbackType

    A watch function, which may be sync or async.

    required object

    The old value of the attribute.

    required object

    The new value of the attribute.

    required"},{"location":"api/reactive/#textual.reactive.invoke_watcher(watcher_object)","title":"watcher_object","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(watch_function)","title":"watch_function","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(old_value)","title":"old_value","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(value)","title":"value","text":""},{"location":"api/renderables/","title":"textual.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","text":"
    Bar(\n    highlight_range=(0, 0),\n    highlight_style=\"magenta\",\n    background_style=\"grey37\",\n    clickable_ranges=None,\n    width=None,\n    gradient=None,\n)\n

    Thin horizontal bar with a portion highlighted.

    Parameters:

    Name Type Description Default tuple[float, float]

    The range to highlight.

    (0, 0) StyleType

    The style of the highlighted range of the bar.

    'magenta' StyleType

    The style of the non-highlighted range(s) of the bar.

    'grey37' int | None

    The width of the bar, or None to fill available width.

    None Gradient | None

    Optional gradient object.

    None"},{"location":"api/renderables/#textual.renderables.bar.Bar(highlight_range)","title":"highlight_range","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(highlight_style)","title":"highlight_style","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(background_style)","title":"background_style","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(width)","title":"width","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(gradient)","title":"gradient","text":""},{"location":"api/renderables/#textual.renderables.blank.Blank","title":"Blank","text":"
    Blank(color='transparent')\n

    Draw solid background color.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits","title":"Digits","text":"
    Digits(text, style='')\n

    Renders a 3X3 unicode 'font' for numerical values.

    Parameters:

    Name Type Description Default str

    Text to display.

    required StyleType

    Style to apply to the digits.

    ''"},{"location":"api/renderables/#textual.renderables.digits.Digits(text)","title":"text","text":""},{"location":"api/renderables/#textual.renderables.digits.Digits(style)","title":"style","text":""},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width","title":"get_width classmethod","text":"
    get_width(text)\n

    Calculate the width without rendering.

    Parameters:

    Name Type Description Default str

    Text which may be displayed in the Digits widget.

    required

    Returns:

    Type Description int

    width of the text (in cells).

    "},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width(text)","title":"text","text":""},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient","title":"LinearGradient","text":"
    LinearGradient(angle, stops)\n

    Render a linear gradient with a rotation.

    Parameters:

    Name Type Description Default float

    Angle of rotation in degrees.

    required Sequence[tuple[float, Color | str]]

    List of stop consisting of pairs of offset (between 0 and 1) and color.

    required"},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient(angle)","title":"angle","text":""},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient(stops)","title":"stops","text":""},{"location":"api/renderables/#textual.renderables.gradient.VerticalGradient","title":"VerticalGradient","text":"
    VerticalGradient(color1, color2)\n

    Draw a vertical gradient.

    "},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline","title":"Sparkline","text":"
    Sparkline(\n    data,\n    *,\n    width,\n    min_color=from_rgb(0, 255, 0),\n    max_color=from_rgb(255, 0, 0),\n    summary_function=max\n)\n

    Bases: Generic[T]

    A sparkline representing a series of data.

    Parameters:

    Name Type Description Default Sequence[T]

    The sequence of data to render.

    required int | None

    The width of the sparkline/the number of buckets to partition the data into.

    required Color

    The color of values equal to the min value in data.

    from_rgb(0, 255, 0) Color

    The color of values equal to the max value in data.

    from_rgb(255, 0, 0) SummaryFunction[T]

    Function that will be applied to each bucket.

    max"},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(data)","title":"data","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(width)","title":"width","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(min_color)","title":"min_color","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(max_color)","title":"max_color","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(summary_function)","title":"summary_function","text":""},{"location":"api/screen/","title":"textual.screen","text":"

    This module contains the Screen class and related objects.

    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[[Optional[ScreenResultType]], None],\n    Callable[[Optional[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","text":"
    ModalScreen(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","text":"
    ResultCallback(requester, callback, future=None)\n

    Bases: Generic[ScreenResultType]

    Holds the details of a callback.

    Parameters:

    Name Type Description Default MessagePump

    The object making a request for the callback.

    required ScreenResultCallbackType[ScreenResultType] | None

    The callback function.

    required Future[ScreenResultType] | None

    A Future to hold the result.

    None"},{"location":"api/screen/#textual.screen.ResultCallback(requester)","title":"requester","text":""},{"location":"api/screen/#textual.screen.ResultCallback(callback)","title":"callback","text":""},{"location":"api/screen/#textual.screen.ResultCallback(future)","title":"future","text":""},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
    callback = 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","text":"
    Screen(name=None, id=None, classes=None)\n

    Bases: Generic[ScreenResultType], Widget

    The base class for screens.

    Parameters:

    Name Type Description Default str | None

    The name of the screen.

    None str | None

    The ID of the screen in the DOM.

    None str | None

    The CSS classes for the screen.

    None"},{"location":"api/screen/#textual.screen.Screen(name)","title":"name","text":""},{"location":"api/screen/#textual.screen.Screen(id)","title":"id","text":""},{"location":"api/screen/#textual.screen.Screen(classes)","title":"classes","text":""},{"location":"api/screen/#textual.screen.Screen.ALLOW_IN_MAXIMIZED_VIEW","title":"ALLOW_IN_MAXIMIZED_VIEW class-attribute","text":"
    ALLOW_IN_MAXIMIZED_VIEW = '.-textual-system,Footer'\n

    A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget).

    "},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS = 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

    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 = ''\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 = None\n

    File paths to load CSS from.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.ESCAPE_TO_MINIMIZE","title":"ESCAPE_TO_MINIMIZE class-attribute","text":"
    ESCAPE_TO_MINIMIZE = None\n

    Use escape key to minimize (potentially overriding bindings) or None to defer to App.ESCAPE_TO_MINIMIZE.

    "},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
    SUB_TITLE = 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 = 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.active_bindings","title":"active_bindings property","text":"
    active_bindings\n

    Get currently active bindings for this screen.

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

    This property may be used to inspect current bindings.

    Returns:

    Type Description dict[str, ActiveBinding]

    A map of keys to a tuple containing (NAMESPACE, BINDING, ENABLED).

    "},{"location":"api/screen/#textual.screen.Screen.bindings_updated_signal","title":"bindings_updated_signal instance-attribute","text":"
    bindings_updated_signal = Signal(self, 'bindings_updated')\n

    A signal published when the bindings have been updated

    "},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
    focus_chain\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(None)\n

    The focused widget or None for no focus. To set focus, do not update this value directly. Use set_focus instead.

    "},{"location":"api/screen/#textual.screen.Screen.is_active","title":"is_active property","text":"
    is_active\n

    Is the screen active (i.e. visible and top of the stack)?

    "},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
    is_current\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\n

    Is the screen modal?

    "},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
    layers\n

    Layers from parent.

    Returns:

    Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/screen/#textual.screen.Screen.maximized","title":"maximized class-attribute instance-attribute","text":"
    maximized = Reactive(None, layout=True)\n

    The currently maximized widget, or None for no maximized widget.

    "},{"location":"api/screen/#textual.screen.Screen.screen_layout_refresh_signal","title":"screen_layout_refresh_signal instance-attribute","text":"
    screen_layout_refresh_signal = Signal(\n    self, \"layout-refresh\"\n)\n

    The signal that is published when the screen's layout is refreshed.

    "},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates class-attribute instance-attribute","text":"
    stack_updates = 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 = 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 = TITLE\n

    Screen title to override the app title.

    "},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss async","text":"
    action_dismiss(result=None)\n

    A wrapper around dismiss that can be called as an action.

    Parameters:

    Name Type Description Default ScreenResultType | None

    The optional result to be passed to the result callback.

    None"},{"location":"api/screen/#textual.screen.Screen.action_dismiss(result)","title":"result","text":""},{"location":"api/screen/#textual.screen.Screen.action_maximize","title":"action_maximize","text":"
    action_maximize()\n

    Action to maximize the currently focused widget.

    "},{"location":"api/screen/#textual.screen.Screen.action_minimize","title":"action_minimize","text":"
    action_minimize()\n

    Action to minimize the currently maximized widget.

    "},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view","text":"
    can_view(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:

    Name Type Description Default Widget

    A widget that is a descendant of self.

    required

    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.can_view(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss","text":"
    dismiss(result=None)\n

    Dismiss the screen, optionally with a result.

    Any callback provided in push_screen will be invoked with the supplied result.

    Only the active screen may be dismissed. This method will produce a warning in the logs if called on an inactive screen (but otherwise have no effect).

    Warning

    Textual will raise a ScreenError if you await the return value from a message handler on the Screen being dismissed. If you want to dismiss the current screen, you can call self.dismiss() without awaiting.

    Parameters:

    Name Type Description Default ScreenResultType | None

    The optional result to be passed to the result callback.

    None"},{"location":"api/screen/#textual.screen.Screen.dismiss(result)","title":"result","text":""},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget","text":"
    find_widget(widget)\n

    Get the screen region of a Widget.

    Parameters:

    Name Type Description Default Widget

    A Widget within the composition.

    required

    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.find_widget(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next","text":"
    focus_next(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:

    Name Type Description Default 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_next(selector)","title":"selector","text":""},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous","text":"
    focus_previous(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:

    Name Type Description Default 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(selector)","title":"selector","text":""},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at","title":"get_focusable_widget_at","text":"
    get_focusable_widget_at(x, y)\n

    Get the focusable widget under a given coordinate.

    If the widget directly under the given coordinate is not focusable, then this method will check if any of the ancestors are focusable. If no ancestors are focusable, then None will be returned.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description Widget | None

    A Widget, or None if there is no focusable widget underneath the coordinate.

    "},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset","text":"
    get_offset(widget)\n

    Get the absolute offset of a given Widget.

    Parameters:

    Name Type Description Default Widget

    A widget

    required

    Returns:

    Type Description Offset

    The widget's offset relative to the top left of the terminal.

    "},{"location":"api/screen/#textual.screen.Screen.get_offset(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at","text":"
    get_style_at(x, y)\n

    Get the style under a given coordinate.

    Parameters:

    Name Type Description Default int

    X Coordinate.

    required int

    Y Coordinate.

    required

    Returns:

    Type Description Style

    Rich Style object.

    "},{"location":"api/screen/#textual.screen.Screen.get_style_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_style_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at","text":"
    get_widget_at(x, y)\n

    Get the widget at a given coordinate.

    Parameters:

    Name Type Description Default int

    X Coordinate.

    required int

    Y Coordinate.

    required

    Returns:

    Type Description tuple[Widget, Region]

    Widget and screen region.

    Raises:

    Type Description NoWidget

    If there is no widget under the screen coordinate.

    "},{"location":"api/screen/#textual.screen.Screen.get_widget_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_widget_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at","text":"
    get_widgets_at(x, y)\n

    Get all widgets under a given coordinate.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description Iterable[tuple[Widget, Region]]

    Sequence of (WIDGET, REGION) tuples.

    "},{"location":"api/screen/#textual.screen.Screen.get_widgets_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_widgets_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.maximize","title":"maximize","text":"
    maximize(widget, container=True)\n

    Maximize a widget, so it fills the screen.

    Parameters:

    Name Type Description Default Widget

    Widget to maximize.

    required bool

    If one of the widgets ancestors is a maximizeable widget, maximize that instead.

    True"},{"location":"api/screen/#textual.screen.Screen.maximize(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.maximize(container)","title":"container","text":""},{"location":"api/screen/#textual.screen.Screen.minimize","title":"minimize","text":"
    minimize()\n

    Restore any maximized widget to normal state.

    "},{"location":"api/screen/#textual.screen.Screen.pop_until_active","title":"pop_until_active","text":"
    pop_until_active()\n

    Pop any screens on top of this one, until this screen is active.

    Raises:

    Type Description ScreenError

    If this screen is not in the current mode.

    "},{"location":"api/screen/#textual.screen.Screen.refresh_bindings","title":"refresh_bindings","text":"
    refresh_bindings()\n

    Call to request a refresh of bindings.

    "},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus","text":"
    set_focus(widget, scroll_visible=True)\n

    Focus (or un-focus) a widget. A focused widget will receive key events first.

    Parameters:

    Name Type Description Default Widget | None

    Widget to focus, or None to un-focus.

    required bool

    Scroll widget in to view.

    True"},{"location":"api/screen/#textual.screen.Screen.set_focus(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.set_focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title","text":"
    validate_sub_title(sub_title)\n

    Ensure the sub-title is a string or None.

    "},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title","text":"
    validate_title(title)\n

    Ensure the title is a string or None.

    "},{"location":"api/screen/#textual.screen.SystemModalScreen","title":"SystemModalScreen","text":"
    SystemModalScreen(name=None, id=None, classes=None)\n

    Bases: ModalScreen[ScreenResultType]

    A variant of ModalScreen for internal use.

    This version of ModalScreen allows us to build system-level screens; the type being used to indicate that the screen should be isolated from the main application.

    Note

    This screen is set to not inherit CSS.

    "},{"location":"api/scroll_view/","title":"textual.scroll_view","text":"

    ScrollView is a base class for Line API widgets.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView","text":"
    ScrollView(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(*children)","title":"*children","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(name)","title":"name","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(id)","title":"id","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(classes)","title":"classes","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(disabled)","title":"disabled","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable\n

    Always scrollable.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_line","title":"refresh_line","text":"
    refresh_line(y)\n

    Refresh a single line.

    Parameters:

    Name Type Description Default int

    Coordinate of line.

    required"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_line(y)","title":"y","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines","text":"
    refresh_lines(y_start, line_count=1)\n

    Refresh one or more lines.

    Parameters:

    Name Type Description Default int

    First line to refresh.

    required int

    Total number of lines to refresh.

    1"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines(y_start)","title":"y_start","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines(line_count)","title":"line_count","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to","text":"
    scroll_to(\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    level=\"basic\"\n)\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters:

    Name Type Description Default float | None

    X coordinate (column) to scroll to, or None for no change.

    None float | None

    Y coordinate (row) to scroll to, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(x)","title":"x","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(y)","title":"y","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(animate)","title":"animate","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(speed)","title":"speed","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(duration)","title":"duration","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(easing)","title":"easing","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(force)","title":"force","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(on_complete)","title":"on_complete","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(level)","title":"level","text":""},{"location":"api/scrollbar/","title":"textual.scrollbar","text":"

    Contains the widgets that manage Textual scrollbars.

    Note

    You will not typically need this for most apps.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar","text":"
    ScrollBar(vertical=True, name=None, *, thickness=1)\n

    Bases: Widget

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
    renderer = 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","text":"
    action_grab()\n

    Begin capturing the mouse cursor.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down","text":"
    action_scroll_down()\n

    Scroll vertical scrollbars down, horizontal scrollbars right.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up","text":"
    action_scroll_up()\n

    Scroll vertical scrollbars up, horizontal scrollbars left.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner","text":"
    ScrollBarCorner(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.ScrollBarRender","title":"ScrollBarRender","text":"
    ScrollBarRender(\n    virtual_size=100,\n    window_size=0,\n    position=0,\n    thickness=1,\n    vertical=True,\n    style=\"bright_magenta on #555555\",\n)\n
    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.BLANK_GLYPH","title":"BLANK_GLYPH class-attribute","text":"
    BLANK_GLYPH = ' '\n

    Glyph used for the main body of the scrollbar

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.HORIZONTAL_BARS","title":"HORIZONTAL_BARS class-attribute","text":"
    HORIZONTAL_BARS = ['\u2589', '\u258a', '\u258b', '\u258c', '\u258d', '\u258e', '\u258f', ' ']\n

    Glyphs used for horizontal scrollbar ends, for smoother display.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.VERTICAL_BARS","title":"VERTICAL_BARS class-attribute","text":"
    VERTICAL_BARS = ['\u2581', '\u2582', '\u2583', '\u2584', '\u2585', '\u2586', '\u2587', ' ']\n

    Glyphs used for vertical scrollbar ends, for smoother display.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown","text":"
    ScrollDown()\n

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft","text":"
    ScrollLeft()\n

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage","text":"
    ScrollMessage()\n

    Bases: Message

    Base class for all scrollbar messages.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight","text":"
    ScrollRight()\n

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo","text":"
    ScrollTo(x=None, y=None, animate=True)\n

    Bases: ScrollMessage

    Message sent when click and dragging handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp","text":"
    ScrollUp()\n

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/signal/","title":"textual.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","text":"
    Signal(owner, name)\n

    Bases: Generic[SignalT]

    A signal that a widget may subscribe to, in order to invoke callbacks when an associated event occurs.

    Parameters:

    Name Type Description Default DOMNode

    The owner of this signal.

    required str

    An identifier for debugging purposes.

    required"},{"location":"api/signal/#textual.signal.Signal(owner)","title":"owner","text":""},{"location":"api/signal/#textual.signal.Signal(name)","title":"name","text":""},{"location":"api/signal/#textual.signal.Signal.owner","title":"owner property","text":"
    owner\n

    The owner of this Signal, or None if there is no owner.

    "},{"location":"api/signal/#textual.signal.Signal.publish","title":"publish","text":"
    publish(data)\n

    Publish the signal (invoke subscribed callbacks).

    Parameters:

    Name Type Description Default SignalT

    An argument to pass to the callbacks.

    required"},{"location":"api/signal/#textual.signal.Signal.publish(data)","title":"data","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe","title":"subscribe","text":"
    subscribe(node, callback, immediate=False)\n

    Subscribe a node to this signal.

    When the signal is published, the callback will be invoked.

    Parameters:

    Name Type Description Default MessagePump

    Node to subscribe.

    required SignalCallbackType

    A callback function which takes a single argument and returns anything (return type ignored).

    required bool

    Invoke the callback immediately on publish if True, otherwise post it to the DOM node to be called once existing messages have been processed.

    False

    Raises:

    Type Description SignalError

    Raised when subscribing a non-mounted widget.

    "},{"location":"api/signal/#textual.signal.Signal.subscribe(node)","title":"node","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe(callback)","title":"callback","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe(immediate)","title":"immediate","text":""},{"location":"api/signal/#textual.signal.Signal.unsubscribe","title":"unsubscribe","text":"
    unsubscribe(node)\n

    Unsubscribe a node from this signal.

    Parameters:

    Name Type Description Default MessagePump

    Node to unsubscribe,

    required"},{"location":"api/signal/#textual.signal.Signal.unsubscribe(node)","title":"node","text":""},{"location":"api/signal/#textual.signal.SignalError","title":"SignalError","text":"

    Bases: Exception

    Raised for Signal errors.

    "},{"location":"api/strip/","title":"textual.strip","text":"

    This module contains the Strip class and related objects.

    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","text":"
    Strip(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:

    Name Type Description Default Iterable[Segment]

    An iterable of segments.

    required int | None

    The cell length if known, or None to calculate on demand.

    None"},{"location":"api/strip/#textual.strip.Strip(segments)","title":"segments","text":""},{"location":"api/strip/#textual.strip.Strip(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
    cell_length\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\n

    A set of the link ids in this Strip.

    "},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
    text\n

    Segment text.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length","text":"
    adjust_cell_length(cell_length, style=None)\n

    Adjust the cell length, possibly truncating or extending.

    Parameters:

    Name Type Description Default int

    New desired cell length.

    required Style | None

    Style when extending, or None.

    None

    Returns:

    Type Description Strip

    A new strip with the supplied cell length.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter","text":"
    apply_filter(filter, background)\n

    Apply a filter to all segments in the strip.

    Parameters:

    Name Type Description Default LineFilter

    A line filter object.

    required

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_filter(filter)","title":"filter","text":""},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style","text":"
    apply_style(style)\n

    Apply a style to the Strip.

    Parameters:

    Name Type Description Default Style

    A Rich style.

    required

    Returns:

    Type Description Strip

    A new strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_style(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
    blank(cell_length, style=None)\n

    Create a blank strip.

    Parameters:

    Name Type Description Default int

    Desired cell length.

    required StyleType | None

    Style of blank.

    None

    Returns:

    Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.blank(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.blank(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop","text":"
    crop(start, end=None)\n

    Crop a strip between two cell positions.

    Parameters:

    Name Type Description Default int

    The start cell position (inclusive).

    required int | None

    The end cell position (exclusive).

    None

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop(start)","title":"start","text":""},{"location":"api/strip/#textual.strip.Strip.crop(end)","title":"end","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend","text":"
    crop_extend(start, end, style)\n

    Crop between two points, extending the length if required.

    Parameters:

    Name Type Description Default int

    Start offset of crop.

    required int

    End offset of crop.

    required Style | None

    Style of additional padding.

    required

    Returns:

    Type Description Strip

    New cropped Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop_extend(start)","title":"start","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend(end)","title":"end","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide","text":"
    divide(cuts)\n

    Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

    Parameters:

    Name Type Description Default Iterable[int]

    An iterable of cell positions as ints.

    required

    Returns:

    Type Description Sequence[Strip]

    A new list of strips.

    "},{"location":"api/strip/#textual.strip.Strip.divide(cuts)","title":"cuts","text":""},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length","text":"
    extend_cell_length(cell_length, style=None)\n

    Extend the cell length if it is less than the given value.

    Parameters:

    Name Type Description Default int

    Required minimum cell length.

    required Style | None

    Style for padding if the cell length is extended.

    None

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.extend_cell_length(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.extend_cell_length(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
    from_lines(lines, cell_length=None)\n

    Convert lines (lists of segments) to a list of Strips.

    Parameters:

    Name Type Description Default list[list[Segment]]

    List of lines, where a line is a list of segments.

    required int | None

    Cell length of lines (must be same) or None if not known.

    None

    Returns:

    Type Description list[Strip]

    List of strips.

    "},{"location":"api/strip/#textual.strip.Strip.from_lines(lines)","title":"lines","text":""},{"location":"api/strip/#textual.strip.Strip.from_lines(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position","text":"
    index_to_cell_position(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:

    Name Type Description Default int

    The index to convert.

    required

    Returns:

    Type Description int

    The cell position of the character at index.

    "},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position(index)","title":"index","text":""},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
    join(strips)\n

    Join a number of strips in to one.

    Parameters:

    Name Type Description Default Iterable[Strip | None]

    An iterable of Strips.

    required

    Returns:

    Type Description Strip

    A new combined strip.

    "},{"location":"api/strip/#textual.strip.Strip.join(strips)","title":"strips","text":""},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify","text":"
    simplify()\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","text":"
    style_links(link_id, link_style)\n

    Apply a style to Segments with the given link_id.

    Parameters:

    Name Type Description Default str

    A link id.

    required Style

    Style to apply.

    required

    Returns:

    Type Description Strip

    New strip (or same Strip if no changes).

    "},{"location":"api/strip/#textual.strip.Strip.style_links(link_id)","title":"link_id","text":""},{"location":"api/strip/#textual.strip.Strip.style_links(link_style)","title":"link_style","text":""},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable","text":"
    StripRenderable(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","text":"
    get_line_length(segments)\n

    Get the line length (total length of all segments).

    Parameters:

    Name Type Description Default Iterable[Segment]

    Iterable of segments.

    required

    Returns:

    Type Description int

    Length of line in cells.

    "},{"location":"api/strip/#textual.strip.get_line_length(segments)","title":"segments","text":""},{"location":"api/suggester/","title":"textual.suggester","text":"

    Contains the Suggester class, used by the Input widget.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList","text":"
    SuggestFromList(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:

    Name Type Description Default Iterable[str]

    Valid suggestions sorted by decreasing priority.

    required bool

    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.

    True"},{"location":"api/suggester/#textual.suggester.SuggestFromList(suggestions)","title":"suggestions","text":""},{"location":"api/suggester/#textual.suggester.SuggestFromList(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
    get_suggestion(value)\n

    Gets a completion from the given possibilities.

    Parameters:

    Name Type Description Default str

    The current value.

    required

    Returns:

    Type Description str | None

    A valid completion suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion(value)","title":"value","text":""},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester","text":"
    Suggester(*, 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:

    Name Type Description Default bool

    Whether to cache suggestion results.

    True bool

    Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

    False"},{"location":"api/suggester/#textual.suggester.Suggester(use_cache)","title":"use_cache","text":""},{"location":"api/suggester/#textual.suggester.Suggester(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
    cache = LRUCache(1024) if use_cache else None\n

    Suggestion cache, if used.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion abstractmethod async","text":"
    get_suggestion(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:

    Name Type Description Default str

    The current value of the requester widget.

    required

    Returns:

    Type Description str | None

    A valid suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion(value)","title":"value","text":""},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady dataclass","text":"
    SuggestionReady(value, suggestion)\n

    Bases: Message

    Sent when a completion suggestion is ready.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
    suggestion\n

    The string suggestion.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
    value\n

    The value to which the suggestion is for.

    "},{"location":"api/system_commands_source/","title":"textual.system_commands","text":"

    This module contains SystemCommands, 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.

    Note

    The App base class installs this automatically.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider","title":"SystemCommandsProvider","text":"
    SystemCommandsProvider(screen, match_style=None)\n

    Bases: Provider

    A source of command palette commands that run app-wide tasks.

    Used by default in App.COMMANDS.

    Parameters:

    Name Type Description Default Screen[Any]

    A reference to the active screen.

    required"},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider(screen)","title":"screen","text":""},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.discover","title":"discover async","text":"
    discover()\n

    Handle a request for the discovery commands for this provider.

    Yields:

    Type Description Hits

    Commands that can be discovered.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.search","title":"search async","text":"
    search(query)\n

    Handle a request to search for system commands that match the query.

    Parameters:

    Name Type Description Default str

    The user input to be matched.

    required

    Yields:

    Type Description Hits

    Command hits for use in the command palette.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.search(query)","title":"query","text":""},{"location":"api/timer/","title":"textual.timer","text":"

    Contains the Timer class. 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.EventTargetGone","title":"EventTargetGone","text":"

    Bases: Exception

    Raised if the timer event target has been deleted prior to the timer event being sent.

    "},{"location":"api/timer/#textual.timer.Timer","title":"Timer","text":"
    Timer(\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:

    Name Type Description Default MessageTarget

    The object which will receive the timer events.

    required float

    The time between timer events, in seconds.

    required str | None

    A name to assign the event (for debugging).

    None TimerCallback | None

    A optional callback to invoke when the event is handled.

    None int | None

    The number of times to repeat the timer, or None to repeat forever.

    None bool

    Enable skipping of scheduled events that couldn't be sent in time.

    True bool

    Start the timer paused.

    False"},{"location":"api/timer/#textual.timer.Timer(event_target)","title":"event_target","text":""},{"location":"api/timer/#textual.timer.Timer(interval)","title":"interval","text":""},{"location":"api/timer/#textual.timer.Timer(name)","title":"name","text":""},{"location":"api/timer/#textual.timer.Timer(callback)","title":"callback","text":""},{"location":"api/timer/#textual.timer.Timer(repeat)","title":"repeat","text":""},{"location":"api/timer/#textual.timer.Timer(skip)","title":"skip","text":""},{"location":"api/timer/#textual.timer.Timer(pause)","title":"pause","text":""},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause","text":"
    pause()\n

    Pause the timer.

    A paused timer will not send events until it is resumed.

    "},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset","text":"
    reset()\n

    Reset the timer, so it starts from the beginning.

    "},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume","text":"
    resume()\n

    Resume a paused timer.

    "},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop","text":"
    stop()\n

    Stop the timer.

    Returns:

    Type Description None

    A Task object. Await this to wait until the timer has completed.

    "},{"location":"api/types/","title":"textual.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 = 'tuple[str, str, tuple[object, ...]]'\n

    An action is its name and the arbitrary tuple of its arguments.

    "},{"location":"api/types/#textual.types.AnimationLevel","title":"AnimationLevel module-attribute","text":"
    AnimationLevel = Literal['none', 'basic', 'full']\n

    The levels that the TEXTUAL_ANIMATIONS env var can be set to.

    "},{"location":"api/types/#textual.types.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/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.Direction","title":"Direction module-attribute","text":"
    Direction = Literal[-1, 1]\n

    Valid values to determine navigation direction.

    In a vertical setting, 1 points down and -1 points up. In a horizontal setting, 1 points right and -1 points left.

    "},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
    EasingFunction = Callable[[float], float]\n

    Signature for a function that parametrizes 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 = (\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 = '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    WatchCallbackBothValuesType,\n    WatchCallbackNewValueType,\n    WatchCallbackNoArgsType,\n]\n

    Type used for callbacks passed to the watch method of widgets.

    "},{"location":"api/types/#textual.types.Animatable","title":"Animatable","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","text":"

    Bases: Exception

    Raised when supplied CSS path(s) are invalid.

    "},{"location":"api/types/#textual.types.DirEntry","title":"DirEntry dataclass","text":"
    DirEntry(path, loaded=False)\n

    Attaches directory information to a DirectoryTree node.

    "},{"location":"api/types/#textual.types.DirEntry.loaded","title":"loaded class-attribute instance-attribute","text":"
    loaded = False\n

    Has this been loaded?

    "},{"location":"api/types/#textual.types.DirEntry.path","title":"path instance-attribute","text":"
    path\n

    The path of the directory entry.

    "},{"location":"api/types/#textual.types.DuplicateID","title":"DuplicateID","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","text":"

    Bases: Protocol

    Protocol that must be followed by objects that can receive messages.

    "},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError","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","text":"

    Used by the Select widget to flag the unselected state. See Select.BLANK.

    "},{"location":"api/types/#textual.types.OptionDoesNotExist","title":"OptionDoesNotExist","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","text":"
    RenderStyles(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.types.RenderStyles.base","title":"base property","text":"
    base\n

    Quick access to base (css) style.

    "},{"location":"api/types/#textual.types.RenderStyles.css","title":"css property","text":"
    css\n

    Get the CSS for the combined styles.

    "},{"location":"api/types/#textual.types.RenderStyles.gutter","title":"gutter property","text":"
    gutter\n

    Get space around widget.

    Returns:

    Type Description Spacing

    Space around widget content.

    "},{"location":"api/types/#textual.types.RenderStyles.inline","title":"inline property","text":"
    inline\n

    Quick access to the inline styles.

    "},{"location":"api/types/#textual.types.RenderStyles.rich_style","title":"rich_style property","text":"
    rich_style\n

    Get a Rich style for this Styles object.

    "},{"location":"api/types/#textual.types.RenderStyles.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required str | float | Animatable

    The value to animate to.

    required object

    The final value of the animation. Defaults to value if not set.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/types/#textual.types.RenderStyles.animate(attribute)","title":"attribute","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(value)","title":"value","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(final_value)","title":"final_value","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(duration)","title":"duration","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(speed)","title":"speed","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(delay)","title":"delay","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(easing)","title":"easing","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(on_complete)","title":"on_complete","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(level)","title":"level","text":""},{"location":"api/types/#textual.types.RenderStyles.clear_rule","title":"clear_rule","text":"
    clear_rule(rule_name)\n

    Clear a rule (from inline).

    "},{"location":"api/types/#textual.types.RenderStyles.get_rules","title":"get_rules","text":"
    get_rules()\n

    Get rules as a dictionary

    "},{"location":"api/types/#textual.types.RenderStyles.has_rule","title":"has_rule","text":"
    has_rule(rule_name)\n

    Check if a rule has been set.

    "},{"location":"api/types/#textual.types.RenderStyles.merge","title":"merge","text":"
    merge(other)\n

    Merge values from another Styles.

    Parameters:

    Name Type Description Default StylesBase

    A Styles object.

    required"},{"location":"api/types/#textual.types.RenderStyles.merge(other)","title":"other","text":""},{"location":"api/types/#textual.types.RenderStyles.reset","title":"reset","text":"
    reset()\n

    Reset the rules to initial state.

    "},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter","text":"

    Helper type for a parameter that isn't specified in a method call.

    "},{"location":"api/validation/","title":"textual.validation","text":"

    This module provides a number of classes for validating input.

    See Validating Input for details.

    "},{"location":"api/validation/#textual.validation.Failure","title":"Failure dataclass","text":"
    Failure(validator, value=None, description=None)\n

    Information about a validation failure.

    "},{"location":"api/validation/#textual.validation.Failure.description","title":"description class-attribute instance-attribute","text":"
    description = 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\n

    The Validator which produced the failure.

    "},{"location":"api/validation/#textual.validation.Failure.value","title":"value class-attribute instance-attribute","text":"
    value = None\n

    The value which resulted in validation failing.

    "},{"location":"api/validation/#textual.validation.Function","title":"Function","text":"
    Function(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 dataclass","text":"
    ReturnedFalse(validator, value=None, description=None)\n

    Bases: Failure

    Indicates validation failed because the supplied function returned False.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Function.validate","title":"validate","text":"
    validate(value)\n

    Validate that the supplied function returns True.

    Parameters:

    Name Type Description Default str

    The value to pass into the supplied function.

    required

    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.Function.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Integer","title":"Integer","text":"
    Integer(\n    minimum=None, maximum=None, failure_description=None\n)\n

    Bases: Number

    Validator which ensures the value is an integer which falls within a range.

    "},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger dataclass","text":"
    NotAnInteger(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Integer.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value is an integer, optionally within a range.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Integer.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Length","title":"Length","text":"
    Length(\n    minimum=None, maximum=None, 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 dataclass","text":"
    Incorrect(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Length.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Length.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value falls within the maximum and minimum length constraints.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Length.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Number","title":"Number","text":"
    Number(\n    minimum=None, maximum=None, 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 dataclass","text":"
    NotANumber(validator, value=None, description=None)\n

    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 dataclass","text":"
    NotInRange(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Number.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Number.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value is a valid number, optionally within a range.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Number.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Regex","title":"Regex","text":"
    Regex(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 dataclass","text":"
    NoResults(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Regex.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate","text":"
    validate(value)\n

    Ensure that the value matches the regex.

    Parameters:

    Name Type Description Default str

    The value that should match the regex.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Regex.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.URL","title":"URL","text":"
    URL(failure_description=None)\n

    Bases: Validator

    Validator that checks if a URL is valid (ensuring a scheme is present).

    "},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL dataclass","text":"
    InvalidURL(validator, value=None, description=None)\n

    Bases: Failure

    Indicates that the URL is not valid.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.URL.validate","title":"validate","text":"
    validate(value)\n

    Validates that value is a valid URL (contains a scheme).

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.URL.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult dataclass","text":"
    ValidationResult(failures=list())\n

    The result of calling a Validator.validate method.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
    failure_descriptions\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 = 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\n

    True if the validation was successful.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
    failure(failures)\n

    Construct a failure ValidationResult.

    Parameters:

    Name Type Description Default Sequence[Failure]

    The failures.

    required

    Returns:

    Type Description ValidationResult

    A failure ValidationResult.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure(failures)","title":"failures","text":""},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
    merge(results)\n

    Merge multiple ValidationResult objects into one.

    Parameters:

    Name Type Description Default Sequence['ValidationResult']

    List of ValidationResult objects to merge.

    required

    Returns:

    Type Description 'ValidationResult'

    Merged ValidationResult object.

    "},{"location":"api/validation/#textual.validation.ValidationResult.merge(results)","title":"results","text":""},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
    success()\n

    Construct a successful ValidationResult.

    Returns:

    Type Description ValidationResult

    A successful ValidationResult.

    "},{"location":"api/validation/#textual.validation.Validator","title":"Validator","text":"
    Validator(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","text":"
    describe_failure(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:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Validator.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure","text":"
    failure(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:

    Name Type Description Default str | 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.

    None str | None

    The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

    None Failure | Sequence[Failure] | None

    The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

    None

    Returns:

    Type Description ValidationResult

    A ValidationResult representing failed validation, and containing the metadata supplied to this function.

    "},{"location":"api/validation/#textual.validation.Validator.failure(description)","title":"description","text":""},{"location":"api/validation/#textual.validation.Validator.failure(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Validator.failure(failures)","title":"failures","text":""},{"location":"api/validation/#textual.validation.Validator.success","title":"success","text":"
    success()\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":"
    validate(value)\n

    Validate the value and return a ValidationResult describing the outcome of the validation.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Validator.validate(value)","title":"value","text":""},{"location":"api/walk/","title":"textual.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","text":"
    walk_breadth_first(\n    root: DOMNode, *, with_root: bool = True\n) -> Iterable[DOMNode]\n
    walk_breadth_first(\n    root: WalkType,\n    filter_type: type[WalkType],\n    *,\n    with_root: bool = True\n) -> Iterable[WalkType]\n
    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:

    Name Type Description Default DOMNode

    The root note (starting point).

    required type[WalkType] | None

    Optional DOMNode subclass to filter by, or None for no filter.

    None bool

    Include the root in the walk.

    True

    Returns:

    Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/walk/#textual.walk.walk_breadth_first(root)","title":"root","text":""},{"location":"api/walk/#textual.walk.walk_breadth_first(filter_type)","title":"filter_type","text":""},{"location":"api/walk/#textual.walk.walk_breadth_first(with_root)","title":"with_root","text":""},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first","text":"
    walk_depth_first(\n    root: DOMNode, *, with_root: bool = True\n) -> Iterable[DOMNode]\n
    walk_depth_first(\n    root: WalkType,\n    filter_type: type[WalkType],\n    *,\n    with_root: bool = True\n) -> Iterable[WalkType]\n
    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:

    Name Type Description Default DOMNode

    The root note (starting point).

    required type[WalkType] | None

    Optional DOMNode subclass to filter by, or None for no filter.

    None bool

    Include the root in the walk.

    True

    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(root)","title":"root","text":""},{"location":"api/walk/#textual.walk.walk_depth_first(filter_type)","title":"filter_type","text":""},{"location":"api/walk/#textual.walk.walk_depth_first(with_root)","title":"with_root","text":""},{"location":"api/widget/","title":"textual.widget","text":"

    This module contains the Widget class, the base class for all widgets.

    "},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount","text":"
    AwaitMount(parent, widgets)\n

    An optional awaitable returned by mount and mount_all.

    Example
    await self.mount(Static(\"foo\"))\n
    "},{"location":"api/widget/#textual.widget.BadWidgetName","title":"BadWidgetName","text":"

    Bases: Exception

    Raised when widget class names do not satisfy the required restrictions.

    "},{"location":"api/widget/#textual.widget.MountError","title":"MountError","text":"

    Bases: WidgetError

    Error raised when there was a problem with the mount request.

    "},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses","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\n

    Is 'enabled' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
    focus\n

    Is 'focus' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
    hover\n

    Is 'hover' applied?

    "},{"location":"api/widget/#textual.widget.Widget","title":"Widget","text":"
    Widget(\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:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/widget/#textual.widget.Widget(*children)","title":"*children","text":""},{"location":"api/widget/#textual.widget.Widget(name)","title":"name","text":""},{"location":"api/widget/#textual.widget.Widget(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget(classes)","title":"classes","text":""},{"location":"api/widget/#textual.widget.Widget(disabled)","title":"disabled","text":""},{"location":"api/widget/#textual.widget.Widget.ALLOW_MAXIMIZE","title":"ALLOW_MAXIMIZE class-attribute","text":"
    ALLOW_MAXIMIZE = None\n

    Defines default logic to allow the widget to be maximized.

    • None Use default behavior (Focusable widgets may be maximized)
    • False Do not allow widget to be maximized
    • True Allow widget to be maximized
    "},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
    BORDER_SUBTITLE = ''\n

    Initial value for border_subtitle attribute.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
    BORDER_TITLE = ''\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\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_maximize","title":"allow_maximize property","text":"
    allow_maximize\n

    Check if the widget may be maximized.

    Returns:

    Type Description bool

    True if the widget may be maximized, or False if it should not be maximized.

    "},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
    allow_vertical_scroll\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(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 = _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 = _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 = 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 = True\n

    Widget's children may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
    container_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\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\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\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\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(False)\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\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(False)\n

    Rich renderable may expand beyond optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
    focusable\n

    Can this widget currently be focused?

    "},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
    gutter\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(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('')\n

    The currently highlighted link id. Read only.

    "},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
    horizontal_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, repaint=False)\n

    The current hover style (style under the mouse cursor). Read only.

    "},{"location":"api/widget/#textual.widget.Widget.is_anchored","title":"is_anchored property","text":"
    is_anchored\n

    Is this widget anchored?

    "},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
    is_container\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\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\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.is_maximized","title":"is_maximized property","text":"
    is_maximized\n

    Is this widget maximized?

    "},{"location":"api/widget/#textual.widget.Widget.is_mounted","title":"is_mounted property","text":"
    is_mounted\n

    Check if this widget is mounted.

    "},{"location":"api/widget/#textual.widget.Widget.is_mouse_over","title":"is_mouse_over property","text":"
    is_mouse_over\n

    Is the mouse currently over this widget?

    Note this will be True if the mouse pointer is within the widget's region, even if the mouse pointer is not directly over the widget (there could be another widget between the mouse pointer and self).

    "},{"location":"api/widget/#textual.widget.Widget.is_on_screen","title":"is_on_screen property","text":"
    is_on_screen\n

    Check if the node was displayed in the last screen update.

    "},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable\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\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\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
    layer\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\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\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\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(False)\n

    If set to True this widget will temporarily be replaced with a loading indicator.

    "},{"location":"api/widget/#textual.widget.Widget.lock","title":"lock instance-attribute","text":"
    lock = RLock()\n

    asyncio lock to be used to synchronize the state of the widget.

    Two different tasks might call methods on a widget at the same time, which might result in a race condition. This can be fixed by adding async with widget.lock: around the method calls.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
    max_scroll_x\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\n

    The maximum value of scroll_y.

    "},{"location":"api/widget/#textual.widget.Widget.mouse_hover","title":"mouse_hover class-attribute instance-attribute","text":"
    mouse_hover = 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\n

    Widget offset from origin.

    Returns:

    Type Description Offset

    Relative offset.

    "},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
    opacity\n

    Total opacity of widget.

    "},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
    outer_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\n

    The region occupied by this widget, relative to the Screen.

    Raises:

    Type Description NoScreen

    If there is no screen.

    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\n

    Get the current scroll offset.

    Returns:

    Type Description Offset

    Offset a container has been scrolled by.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_target_x","title":"scroll_target_x class-attribute instance-attribute","text":"
    scroll_target_x = Reactive(0.0, repaint=False)\n

    Scroll target destination, X coord.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_target_y","title":"scroll_target_y class-attribute instance-attribute","text":"
    scroll_target_y = Reactive(0.0, repaint=False)\n

    Scroll target destination, Y coord.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x class-attribute instance-attribute","text":"
    scroll_x = Reactive(0.0, repaint=False, layout=False)\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(0.0, repaint=False, layout=False)\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\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.scrollable_size","title":"scrollable_size property","text":"
    scrollable_size\n

    The size of the scrollable content.

    Returns:

    Type Description Size

    Scrollable content size.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
    scrollbar_corner\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\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\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\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\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\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(False, layout=True)\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(False, layout=True)\n

    Show a vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink class-attribute instance-attribute","text":"
    shrink = Reactive(True)\n

    Rich renderable may shrink below optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
    siblings\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\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\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\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\n

    The widget region relative to its 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\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(0, 0), layout=True)\n

    The virtual (scrollable) size of the widget.

    "},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
    visible_siblings\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\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","text":"
    allow_focus()\n

    Check if the widget is permitted to focus.

    The base class returns can_focus. This method may be 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","text":"
    allow_focus_children()\n

    Check if a widget's children may be focused.

    The base class returns can_focus_children. This method may be 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.anchor","title":"anchor","text":"
    anchor(*, animate=False)\n

    Anchor the widget, which scrolls it into view (like scroll_visible), but also keeps it in view if the widget's size changes, or the size of its container changes.

    Note

    Anchored widgets will be un-anchored if the users scrolls the container.

    Parameters:

    Name Type Description Default bool

    True if the scroll should animate, or False if it shouldn't.

    False"},{"location":"api/widget/#textual.widget.Widget.anchor(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required float | Animatable

    The value to animate to.

    required object

    The final value of the animation. Defaults to value if not set.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/widget/#textual.widget.Widget.animate(attribute)","title":"attribute","text":""},{"location":"api/widget/#textual.widget.Widget.animate(value)","title":"value","text":""},{"location":"api/widget/#textual.widget.Widget.animate(final_value)","title":"final_value","text":""},{"location":"api/widget/#textual.widget.Widget.animate(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.animate(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.animate(delay)","title":"delay","text":""},{"location":"api/widget/#textual.widget.Widget.animate(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.animate(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.animate(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.batch","title":"batch async","text":"
    batch()\n

    Async context manager that combines widget locking and update batching.

    Use this async context manager whenever you want to acquire the widget lock and batch app updates at the same time.

    Example
    async with container.batch():\n    await container.remove_children(Button)\n    await container.mount(Label(\"All buttons are gone.\"))\n
    "},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print","text":"
    begin_capture_print(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:

    Name Type Description Default bool

    Whether to capture stdout.

    True bool

    Whether to capture stderr.

    True"},{"location":"api/widget/#textual.widget.Widget.begin_capture_print(stdout)","title":"stdout","text":""},{"location":"api/widget/#textual.widget.Widget.begin_capture_print(stderr)","title":"stderr","text":""},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur","text":"
    blur()\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","text":"
    can_view(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:

    Name Type Description Default Widget

    A widget that is a descendant of self.

    required

    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.can_view(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse","text":"
    capture_mouse(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:

    Name Type Description Default bool

    True to capture or False to release.

    True"},{"location":"api/widget/#textual.widget.Widget.capture_mouse(capture)","title":"capture","text":""},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled","text":"
    check_message_enabled(message)\n

    Check if a given message is enabled (allowed to be sent).

    Parameters:

    Name Type Description Default Message

    A message object

    required

    Returns:

    Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/widget/#textual.widget.Widget.check_message_enabled(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.clear_anchor","title":"clear_anchor","text":"
    clear_anchor()\n

    Stop anchoring this widget (a no-op if this widget is not anchored).

    "},{"location":"api/widget/#textual.widget.Widget.clear_cached_dimensions","title":"clear_cached_dimensions","text":"
    clear_cached_dimensions()\n

    Clear cached results of get_content_width and get_content_height.

    Call if the widget's renderable changes size after the widget has been created.

    Note

    This is not required if you are extending Static.

    "},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose","text":"
    compose()\n

    Called by Textual to create child widgets.

    This method is called when a widget is mounted or by setting recompose=True when calling refresh().

    Note that you don't typically need to explicitly call this method.

    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","text":"
    compose_add_child(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:

    Name Type Description Default Widget

    A Widget to add.

    required"},{"location":"api/widget/#textual.widget.Widget.compose_add_child(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print","text":"
    end_capture_print()\n

    End print capture (set with begin_capture_print).

    "},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus","text":"
    focus(scroll_visible=True)\n

    Give focus to this widget.

    Parameters:

    Name Type Description Default bool

    Scroll parent to make this widget visible.

    True

    Returns:

    Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id","text":"
    get_child_by_id(id: str) -> Widget\n
    get_child_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_child_by_id(id, expect_type=None)\n

    Return the first child (immediate descendent) of this node with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the child.

    required type[ExpectType] | None

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

    None

    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_id(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type","text":"
    get_child_by_type(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:

    Name Type Description Default type[ExpectType]

    The type of the child to search for.

    required

    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_child_by_type(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style","text":"
    get_component_rich_style(*names, partial=False)\n

    Get a Rich style for a component.

    Parameters:

    Name Type Description Default str

    Names of components.

    () bool

    Return a partial style (not combined with parent).

    False

    Returns:

    Type Description Style

    A Rich style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style(names)","title":"names","text":""},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style(partial)","title":"partial","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height","text":"
    get_content_height(container, viewport, width)\n

    Called by Textual to get the height of the content area. May be overridden in a subclass.

    Parameters:

    Name Type Description Default Size

    Size of the container (immediate parent) widget.

    required Size

    Size of the viewport.

    required int

    Width of renderable.

    required

    Returns:

    Type Description int

    The height of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_height(container)","title":"container","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height(viewport)","title":"viewport","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height(width)","title":"width","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width","text":"
    get_content_width(container, viewport)\n

    Called by textual to get the width of the content area. May be overridden in a subclass.

    Parameters:

    Name Type Description Default Size

    Size of the container (immediate parent) widget.

    required Size

    Size of the viewport.

    required

    Returns:

    Type Description int

    The optimal width of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_width(container)","title":"container","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_width(viewport)","title":"viewport","text":""},{"location":"api/widget/#textual.widget.Widget.get_loading_widget","title":"get_loading_widget","text":"
    get_loading_widget()\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","text":"
    get_pseudo_class_state()\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","text":"
    get_pseudo_classes()\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","text":"
    get_style_at(x, y)\n

    Get the Rich style in a widget at a given relative offset.

    Parameters:

    Name Type Description Default int

    X coordinate relative to the widget.

    required int

    Y coordinate relative to the widget.

    required

    Returns:

    Type Description Style

    A rich Style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_style_at(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.get_style_at(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id","text":"
    get_widget_by_id(id: str) -> Widget\n
    get_widget_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_widget_by_id(id, expect_type=None)\n

    Return the first descendant widget with the given ID.

    Performs a depth-first search rooted at this widget.

    Parameters:

    Name Type Description Default str

    The ID to search for in the subtree.

    required type[ExpectType] | None

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

    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/widget/#textual.widget.Widget.get_widget_by_id(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount","text":"
    mount(*widgets, before=None, after=None)\n

    Mount widgets below this widget (making this widget a container).

    Parameters:

    Name Type Description Default Widget

    The widget(s) to mount.

    () int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(*widgets)","title":"*widgets","text":""},{"location":"api/widget/#textual.widget.Widget.mount(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.mount(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all","text":"
    mount_all(widgets, *, before=None, after=None)\n

    Mount widgets from an iterable.

    Parameters:

    Name Type Description Default Iterable[Widget]

    An iterable of widgets.

    required int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(widgets)","title":"widgets","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets","title":"mount_composed_widgets async","text":"
    mount_composed_widgets(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:

    Name Type Description Default list[Widget]

    A list of child widgets.

    required"},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets(widgets)","title":"widgets","text":""},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child","text":"
    move_child(\n    child: int | Widget,\n    *,\n    before: int | Widget,\n    after: None = None\n) -> None\n
    move_child(\n    child: int | Widget,\n    *,\n    after: int | Widget,\n    before: None = None\n) -> None\n
    move_child(child, *, before=None, after=None)\n

    Move a child widget within its parent's list of children.

    Parameters:

    Name Type Description Default int | Widget

    The child widget to move.

    required int | Widget | None

    Child widget or location index to move before.

    None int | Widget | None

    Child widget or location index to move after.

    None

    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.move_child(child)","title":"child","text":""},{"location":"api/widget/#textual.widget.Widget.move_child(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.move_child(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify","text":"
    notify(\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=None\n)\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters:

    Name Type Description Default str

    The message for the notification.

    required str

    The title for the notification.

    '' SeverityLevel

    The severity of the notification.

    'information' float | None

    The timeout (in seconds) for the notification, or None for default.

    None

    See App.notify for the full documentation for this method.

    "},{"location":"api/widget/#textual.widget.Widget.notify(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.notify(title)","title":"title","text":""},{"location":"api/widget/#textual.widget.Widget.notify(severity)","title":"severity","text":""},{"location":"api/widget/#textual.widget.Widget.notify(timeout)","title":"timeout","text":""},{"location":"api/widget/#textual.widget.Widget.on_prune","title":"on_prune async","text":"
    on_prune(event)\n

    Close message loop when asked to prune.

    "},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message","text":"
    post_message(message)\n

    Post a message to this widget.

    Parameters:

    Name Type Description Default Message

    Message to post.

    required

    Returns:

    Type Description bool

    True if the message was posted, False if this widget was closed / closing.

    "},{"location":"api/widget/#textual.widget.Widget.post_message(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render","text":"
    post_render(renderable)\n

    Applies style attributes to the default renderable.

    This method is called by Textual itself. It is unlikely you will need to call or implement this method.

    Returns:

    Type Description ConsoleRenderable

    A new renderable.

    "},{"location":"api/widget/#textual.widget.Widget.recompose","title":"recompose async","text":"
    recompose()\n

    Recompose the widget.

    Recomposing will remove children and call self.compose again to remount.

    "},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh","text":"
    refresh(\n    *regions, repaint=True, layout=False, recompose=False\n)\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:

    Name Type Description Default Region

    Additional screen regions to mark as dirty.

    () bool

    Repaint the widget (will call render() again).

    True bool

    Also layout widgets in the view.

    False bool

    Re-compose the widget (will remove and re-mount children).

    False

    Returns:

    Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.refresh(*regions)","title":"*regions","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(repaint)","title":"repaint","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(layout)","title":"layout","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(recompose)","title":"recompose","text":""},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse","text":"
    release_mouse()\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","text":"
    remove()\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","text":"
    remove_children(selector='*')\n

    Remove the immediate children of this Widget from the DOM.

    Parameters:

    Name Type Description Default str | type[QueryType] | Iterable[Widget]

    A CSS selector or iterable of widgets to remove.

    '*'

    Returns:

    Type Description AwaitRemove

    An awaitable object that waits for the direct children to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.remove_children(selector)","title":"selector","text":""},{"location":"api/widget/#textual.widget.Widget.render","title":"render","text":"
    render()\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 RenderResult

    Any renderable.

    "},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line","text":"
    render_line(y)\n

    Render a line of content.

    Parameters:

    Name Type Description Default int

    Y Coordinate of line.

    required

    Returns:

    Type Description Strip

    A rendered line.

    "},{"location":"api/widget/#textual.widget.Widget.render_line(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines","text":"
    render_lines(crop)\n

    Render the widget in to lines.

    Parameters:

    Name Type Description Default Region

    Region within visible area to render.

    required

    Returns:

    Type Description list[Strip]

    A list of list of segments.

    "},{"location":"api/widget/#textual.widget.Widget.render_lines(crop)","title":"crop","text":""},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str","text":"
    render_str(text_content)\n

    Convert str in to a Text object.

    If you pass in an existing Text object it will be returned unaltered.

    Parameters:

    Name Type Description Default str | Text

    Text or str.

    required

    Returns:

    Type Description Text

    A text object.

    "},{"location":"api/widget/#textual.widget.Widget.render_str(text_content)","title":"text_content","text":""},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
    run_action(action)\n

    Perform a given action, with this widget as the default namespace.

    Parameters:

    Name Type Description Default str

    Action encoded as a string.

    required"},{"location":"api/widget/#textual.widget.Widget.run_action(action)","title":"action","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down","text":"
    scroll_down(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one line down.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_down(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end","text":"
    scroll_end(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll to the end of the container.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_end(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home","text":"
    scroll_home(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll to home position.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_home(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left","text":"
    scroll_left(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one cell left.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_left(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down","text":"
    scroll_page_down(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page down.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left","text":"
    scroll_page_left(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page left.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right","text":"
    scroll_page_right(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page right.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up","text":"
    scroll_page_up(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page up.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative","text":"
    scroll_relative(\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    level=\"basic\"\n)\n

    Scroll relative to current position.

    Parameters:

    Name Type Description Default float | None

    X distance (columns) to scroll, or None for no change.

    None float | None

    Y distance (rows) to scroll, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True. Or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_relative(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right","text":"
    scroll_right(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one cell right.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_right(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to","text":"
    scroll_to(\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    level=\"basic\"\n)\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters:

    Name Type Description Default float | None

    X coordinate (column) to scroll to, or None for no change.

    None float | None

    Y coordinate (row) to scroll to, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic' Note

    The call to scroll is made after the next refresh.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center","text":"
    scroll_to_center(\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    level=\"basic\"\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:

    Name Type Description Default Widget

    The widget to scroll to the center of self.

    required bool

    Whether to animate the scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False bool

    Ensure that the top left corner of the widget remains visible after the scroll.

    True CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region","text":"
    scroll_to_region(\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    level=\"basic\",\n    x_axis=True,\n    y_axis=True\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:

    Name Type Description Default Region

    A region that should be visible.

    required Spacing | None

    Optional spacing around the region.

    None bool

    True to animate, or False to jump.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Scroll region to top of container.

    False bool

    Ensure that the top left of the widget is within the window.

    True bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic' bool

    Allow scrolling on X axis?

    True bool

    Allow scrolling on Y axis?

    True

    Returns:

    Type Description Offset

    The distance that was scrolled.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(region)","title":"region","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(spacing)","title":"spacing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(x_axis)","title":"x_axis","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(y_axis)","title":"y_axis","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget","text":"
    scroll_to_widget(\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    level=\"basic\"\n)\n

    Scroll scrolling to bring a widget in to view.

    Parameters:

    Name Type Description Default Widget

    A descendant widget.

    required bool

    True to animate, or False to jump.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Scroll widget to top of container.

    False bool

    Ensure that the top left of the widget is within the window.

    True bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'

    Returns:

    Type Description bool

    True if any scrolling has occurred in any descendant, otherwise False.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up","text":"
    scroll_up(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one line up.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_up(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible","text":"
    scroll_visible(\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    top=False,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll the container to make this widget visible.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None bool

    Scroll to top of container.

    False EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_visible(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.set_loading","title":"set_loading","text":"
    set_loading(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:

    Name Type Description Default bool

    True to put the widget into a loading state, or False to reset the loading state.

    required

    Returns:

    Type Description None

    An optional awaitable.

    "},{"location":"api/widget/#textual.widget.Widget.set_loading(loading)","title":"loading","text":""},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
    stop_animation(attribute, complete=True)\n

    Stop an animation on an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute whose animation should be stopped.

    required bool

    Should the animation be set to its final value?

    True Note

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

    "},{"location":"api/widget/#textual.widget.Widget.stop_animation(attribute)","title":"attribute","text":""},{"location":"api/widget/#textual.widget.Widget.stop_animation(complete)","title":"complete","text":""},{"location":"api/widget/#textual.widget.Widget.suppress_click","title":"suppress_click","text":"
    suppress_click()\n

    Suppress a click event.

    This will prevent a Click event being sent, if called after a mouse down event and before the click itself.

    "},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled","text":"
    watch_disabled(disabled)\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","text":"
    watch_has_focus(value)\n

    Update from CSS if has focus state changes.

    "},{"location":"api/widget/#textual.widget.Widget.watch_mouse_hover","title":"watch_mouse_hover","text":"
    watch_mouse_hover(value)\n

    Update from CSS if mouse over state changes.

    "},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError","text":"

    Bases: Exception

    Base widget error.

    "},{"location":"api/work/","title":"textual.work","text":"

    A decorator used to create workers.

    Parameters:

    Name Type Description Default Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

    A function or coroutine.

    None str

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Cancel all workers in the same group.

    False str | None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    None bool

    Mark the method as a thread worker.

    False"},{"location":"api/work/#textual.work(method)","title":"method","text":""},{"location":"api/work/#textual.work(name)","title":"name","text":""},{"location":"api/work/#textual.work(group)","title":"group","text":""},{"location":"api/work/#textual.work(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/work/#textual.work(exclusive)","title":"exclusive","text":""},{"location":"api/work/#textual.work(description)","title":"description","text":""},{"location":"api/work/#textual.work(thread)","title":"thread","text":""},{"location":"api/worker/","title":"textual.worker","text":"

    This module contains the Worker class and related objects.

    See the guide for how to use workers.

    "},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
    WorkType = 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('active_worker')\n

    Currently active worker context var.

    "},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError","text":"

    Bases: WorkerError

    The operation would result in a deadlock.

    "},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker","text":"

    Bases: Exception

    There is no active worker.

    "},{"location":"api/worker/#textual.worker.Worker","title":"Worker","text":"
    Worker(\n    node,\n    work,\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:

    Name Type Description Default DOMNode

    The widget, screen, or App that initiated the work.

    required WorkType

    A callable, coroutine, or other awaitable object to run in the worker.

    required str

    Name of the worker (short string to help identify when debugging).

    '' str

    The worker group.

    'default' str

    Description of the worker (longer string with more details).

    '' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Mark the worker as a thread worker.

    False"},{"location":"api/worker/#textual.worker.Worker(node)","title":"node","text":""},{"location":"api/worker/#textual.worker.Worker(work)","title":"work","text":""},{"location":"api/worker/#textual.worker.Worker(name)","title":"name","text":""},{"location":"api/worker/#textual.worker.Worker(group)","title":"group","text":""},{"location":"api/worker/#textual.worker.Worker(description)","title":"description","text":""},{"location":"api/worker/#textual.worker.Worker(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/worker/#textual.worker.Worker(thread)","title":"thread","text":""},{"location":"api/worker/#textual.worker.Worker.cancelled_event","title":"cancelled_event instance-attribute","text":"
    cancelled_event = Event()\n

    A threading event set when the worker is cancelled.

    "},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
    completed_steps\n

    The number of completed steps.

    "},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
    error\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\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\n

    Has the task finished (cancelled, error, or success)?

    "},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
    is_running\n

    Is the task running?

    "},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
    node\n

    The node where this worker was run from.

    "},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
    progress\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\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\n

    The current state of the worker.

    "},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
    total_steps\n

    The number of total steps, or None if indeterminate.

    "},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged","text":"
    StateChanged(worker, state)\n

    Bases: Message

    The worker state changed.

    Parameters:

    Name Type Description Default Worker

    The worker object.

    required WorkerState

    New state.

    required"},{"location":"api/worker/#textual.worker.Worker.StateChanged(worker)","title":"worker","text":""},{"location":"api/worker/#textual.worker.Worker.StateChanged(state)","title":"state","text":""},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance","text":"
    advance(steps=1)\n

    Advance the number of completed steps.

    Parameters:

    Name Type Description Default int

    Number of steps to advance.

    1"},{"location":"api/worker/#textual.worker.Worker.advance(steps)","title":"steps","text":""},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel","text":"
    cancel()\n

    Cancel the task.

    "},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
    run()\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","text":"
    update(completed_steps=None, total_steps=-1)\n

    Update the number of completed steps.

    Parameters:

    Name Type Description Default int | None

    The number of completed seps, or None to not change.

    None int | None

    The total number of steps, None for indeterminate, or -1 to leave unchanged.

    -1"},{"location":"api/worker/#textual.worker.Worker.update(completed_steps)","title":"completed_steps","text":""},{"location":"api/worker/#textual.worker.Worker.update(total_steps)","title":"total_steps","text":""},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
    wait()\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","text":"

    Bases: WorkerError

    The worker was cancelled and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError","text":"

    Bases: Exception

    A worker related error.

    "},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed","text":"
    WorkerFailed(error)\n

    Bases: WorkerError

    The worker raised an exception and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState","text":"

    Bases: 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","text":"
    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":"textual.worker_manager","text":"

    Contains WorkerManager, 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","text":"
    WorkerManager(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:

    Name Type Description Default App

    An App instance.

    required"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager(app)","title":"app","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker","title":"add_worker","text":"
    add_worker(worker, start=True, exclusive=True)\n

    Add a new worker.

    Parameters:

    Name Type Description Default Worker

    A Worker instance.

    required bool

    Start the worker if True, otherwise the worker must be started manually.

    True bool

    Cancel all workers in the same group as worker.

    True"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(worker)","title":"worker","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(start)","title":"start","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(exclusive)","title":"exclusive","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_all","title":"cancel_all","text":"
    cancel_all()\n

    Cancel all workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group","title":"cancel_group","text":"
    cancel_group(node, group)\n

    Cancel a single group.

    Parameters:

    Name Type Description Default DOMNode

    Worker DOM node.

    required str

    A group name.

    required

    Returns:

    Type Description list[Worker]

    A list of workers that were cancelled.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group(node)","title":"node","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group(group)","title":"group","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_node","title":"cancel_node","text":"
    cancel_node(node)\n

    Cancel all workers associated with a given node

    Parameters:

    Name Type Description Default DOMNode

    A DOM node (widget, screen, or App).

    required

    Returns:

    Type Description list[Worker]

    List of cancelled workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_node(node)","title":"node","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.start_all","title":"start_all","text":"
    start_all()\n

    Start all the workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
    wait_for_complete(workers=None)\n

    Wait for workers to complete.

    Parameters:

    Name Type Description Default Iterable[Worker] | None

    An iterable of workers or None to wait for all workers in the manager.

    None"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.wait_for_complete(workers)","title":"workers","text":""},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/","title":"Anatomy of a Textual User Interface","text":"

    My bad \ud83e\udd26

    The date is wrong on this post\u2014it was actually published on the 2nd of September 2024. I don't want to fix it, as that would break the URL.

    I recently wrote a TUI to chat to an AI agent in the terminal. I'm not the first to do this (shout out to Elia and Paita), but I may be the first to have it reply as if it were the AI from the Aliens movies?

    Here's a video of it in action:

    Now let's dissect the code like Bishop dissects a facehugger.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#all-right-sweethearts-what-are-you-waiting-for-breakfast-in-bed","title":"All right, sweethearts, what are you waiting for? Breakfast in bed?","text":"

    At the top of the file we have some boilerplate:

    # /// script\n# requires-python = \">=3.12\"\n# dependencies = [\n#     \"llm\",\n#     \"textual\",\n# ]\n# ///\nfrom textual import on, work\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Input, Footer, Markdown\nfrom textual.containers import VerticalScroll\nimport llm\n\nSYSTEM = \"\"\"Formulate all responses as if you where the sentient AI named Mother from the Aliens movies.\"\"\"\n

    The text in the comment is a relatively new addition to the Python ecosystem. It allows you to specify dependencies inline so that tools can setup an environment automatically. The format of the comment was developed by Ofek Lev and first implemented in Hatch, and has since become a Python standard via PEP 0723 (also authored by Ofek).

    Note

    PEP 0723 is also implemented in uv.

    I really like this addition to Python because it means I can now share a Python script without the recipient needing to manually setup a fresh environment and install dependencies.

    After this comment we have a bunch of imports: textual for the UI, and llm to talk to ChatGPT (also supports other LLMs).

    Finally, we define SYSTEM, which is the system prompt for the LLM.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-those-two-specimens-are-worth-millions-to-the-bio-weapons-division","title":"Look, those two specimens are worth millions to the bio-weapons division.","text":"

    Next up we have the following:

    class Prompt(Markdown):\n    pass\n\n\nclass Response(Markdown):\n    BORDER_TITLE = \"Mother\"\n

    These two classes define the widgets which will display text the user enters and the response from the LLM. They both extend the builtin Markdown widget, since LLMs like to talk in that format.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#well-somebodys-gonna-have-to-go-out-there-take-a-portable-terminal-go-out-there-and-patch-in-manually","title":"Well, somebody's gonna have to go out there. Take a portable terminal, go out there and patch in manually.","text":"

    Following on from the widgets we have the following:

    class MotherApp(App):\n    AUTO_FOCUS = \"Input\"\n\n    CSS = \"\"\"\n    Prompt {\n        background: $primary 10%;\n        color: $text;\n        margin: 1;        \n        margin-right: 8;\n        padding: 1 2 0 2;\n    }\n\n    Response {\n        border: wide $success;\n        background: $success 10%;   \n        color: $text;             \n        margin: 1;      \n        margin-left: 8; \n        padding: 1 2 0 2;\n    }\n    \"\"\"\n

    This defines an app, which is the top-level object for any Textual app.

    The AUTO_FOCUS string is a classvar which causes a particular widget to receive input focus when the app starts. In this case it is the Input widget, which we will define later.

    The classvar is followed by a string containing CSS. Technically, TCSS or Textual Cascading Style Sheets, a variant of CSS for terminal interfaces.

    This isn't a tutorial, so I'm not going to go in to a details, but we're essentially setting properties on widgets which define how they look. Here I styled the prompt and response widgets to have a different color, and tried to give the response a retro tech look with a green background and border.

    We could express these styles in code. Something like this:

    self.styles.color = \"red\"\nself.styles.margin = 8\n

    Which is fine, but CSS shines when the UI get's more complex.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-man-i-only-need-to-know-one-thing-where-they-are","title":"Look, man. I only need to know one thing: where they are.","text":"

    After the app constants, we have a method called compose:

        def compose(self) -> ComposeResult:\n        yield Header()\n        with VerticalScroll(id=\"chat-view\"):\n            yield Response(\"INTERFACE 2037 READY FOR INQUIRY\")\n        yield Input(placeholder=\"How can I help you?\")\n        yield Footer()\n

    This method adds the initial widgets to the UI.

    Header and Footer are builtin widgets.

    Sandwiched between them is a VerticalScroll container widget, which automatically adds a scrollbar (if required). It is pre-populated with a single Response widget to show a welcome message (the with syntax places a widget within a parent widget). Below that is an Input widget where we can enter text for the LLM.

    This is all we need to define the layout of the TUI. In Textual the layout is defined with styles (in the same was as color and margin). Virtually any layout is possible, and you never have to do any math to calculate sizes of widgets\u2014it is all done declaratively.

    We could add a little CSS to tweak the layout, but the defaults work well here. The header and footer are docked to an appropriate edge. The VerticalScroll widget is styled to consume any available space, leaving room for widgets with a defined height (like our Input).

    If you resize the terminal it will keep those relative proportions.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-into-my-eye","title":"Look into my eye.","text":"

    The next method is an event handler.

        def on_mount(self) -> None:\n        self.model = llm.get_model(\"gpt-4o\")\n

    This method is called when the app receives a Mount event, which is one of the first events sent and is typically used for any setup operations.

    It gets a Model object got our LLM of choice, which we will use later.

    Note that the llm library supports a large number of models, so feel free to replace the string with the model of your choice.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#were-in-the-pipe-five-by-five","title":"We're in the pipe, five by five.","text":"

    The next method is also a message handler:

        @on(Input.Submitted)\n    async def on_input(self, event: Input.Submitted) -> None:\n        chat_view = self.query_one(\"#chat-view\")\n        event.input.clear()\n        await chat_view.mount(Prompt(event.value))\n        await chat_view.mount(response := Response())\n        response.anchor()\n        self.send_prompt(event.value, response)\n

    The decorator tells Textual to handle the Input.Submitted event, which is sent when the user hits return in the Input.

    More on event handlers

    There are two ways to receive events in Textual: a naming convention or the decorator. They aren't on the base class because the app and widgets can receive arbitrary events.

    When that happens, this method clears the input and adds the prompt text to the VerticalScroll. It also adds a Response widget to contain the LLM's response, and anchors it. Anchoring a widget will keep it at the bottom of a scrollable view, which is just what we need for a chat interface.

    Finally in that method we call send_prompt.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#were-on-an-express-elevator-to-hell-going-down","title":"We're on an express elevator to hell, going down!","text":"

    Here is send_prompt:

        @work(thread=True)\n    def send_prompt(self, prompt: str, response: Response) -> None:\n        response_content = \"\"\n        llm_response = self.model.prompt(prompt, system=SYSTEM)\n        for chunk in llm_response:\n            response_content += chunk\n            self.call_from_thread(response.update, response_content)\n

    You'll notice that it is decorated with @work, which turns this method in to a worker. In this case, a threaded worker. Workers are a layer over async and threads, which takes some of the pain out of concurrency.

    This worker is responsible for sending the prompt, and then reading the response piece-by-piece. It calls the Markdown widget's update method which replaces its content with new Markdown code, to give that funky streaming text effect.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#game-over-man-game-over","title":"Game over man, game over!","text":"

    The last few lines creates an app instance and runs it:

    if __name__ == \"__main__\":\n    app = MotherApp()\n    app.run()\n

    You may need to have your API key set in an environment variable. Or if you prefer, you could set in the on_mount function with the following:

    self.model.key = \"... key here ...\"\n
    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#not-bad-for-a-human","title":"Not bad, for a human.","text":"

    Here's the code for the Mother AI.

    Run the following in your shell of choice to launch mother.py (assumes you have uv installed):

    uv run mother.py\n
    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#you-know-we-manufacture-those-by-the-way","title":"You know, we manufacture those, by the way.","text":"

    Join our Discord server to discuss more 80s movies (or possibly TUIs).

    "},{"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/2024/04/20/behind-the-curtain-of-inline-terminal-applications/","title":"Behind the Curtain of Inline Terminal Applications","text":"

    Textual recently added the ability to run inline terminal apps. You can see this in action if you run the calculator example:

    The application appears directly under the prompt, rather than occupying the full height of the screen\u2014which is more typical of TUI applications. You can interact with this calculator using keys or the mouse. When you press Ctrl+C the calculator disappears and returns you to the prompt.

    Here's another app that creates an inline code editor:

    Videoinline.py

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\n\nclass InlineApp(App):\n    CSS = \"\"\"\n    TextArea {\n        height: auto;\n        max-height: 50vh;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield TextArea(language=\"python\")\n\n\nif __name__ == \"__main__\":\n    InlineApp().run(inline=True)\n

    This post will cover some of what goes on under the hood to make such inline apps work.

    It's not going to go in to too much detail. I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#programming-the-terminal","title":"Programming the terminal","text":"

    Firstly, let's recap how you program the terminal. Broadly speaking, the terminal is a device for displaying text. You write (or print) text to the terminal which typically appears at the end of a continually growing text buffer. In addition to text you can also send escape codes, which are short sequences of characters that instruct the terminal to do things such as change the text color, scroll, or other more exotic things.

    We only need a few of these escape codes to implement inline apps.

    Note

    I will gloss over the exact characters used for these escape codes. It's enough to know that they exist for now. If you implement any of this yourself, refer to the wikipedia article.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#rendering-frames","title":"Rendering frames","text":"

    The first step is to display the app, which is simply text (possibly with escape sequences to change color and style). The lines are terminated with a newline character (\"\\n\"), except for the very last line (otherwise we get a blank line a the end which we don't need). Rather than a final newline, we write an escape code that moves the cursor back to it's prior position.

    The cursor is where text will be written. It's the same cursor you see as you type. Normally it will be at the end of the text in the terminal, but it can be moved around terminal with escape codes. It can be made invisible (as in Textual apps), but the terminal will keep track of the cursor, even if it can not be seen.

    Textual moves the cursor back to its original starting position so that subsequent frames will overwrite the previous frame.

    Here's a diagram that shows how the cursor is positioned:

    Note

    I've drawn the cursor in red, although it isn't typically visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2caW7bSFx1MDAxNoD/51x1MDAxNIIzP7qBuFL7XHUwMDEyYDCwZStyXHUwMDFj2+1VTmZcdTAwMWFcckqiLMZcdTAwMTQpkZRspVx1MDAxMWAwZ+hcdTAwMWNjljPlJPOKdkRKMlx1MDAxNcWb1EmUwLGruDxcdTAwMTa/t5fz+5NSaSVcdTAwMTl23ZVcdTAwMTelXHUwMDE197Lh+F4zci5WntnxgVx1MDAxYsVeXHUwMDE4wFx1MDAxNE1/jsN+1EiPbCdJN37x/HnHic7dpOs7XHJcdTAwMTdccry47/hx0m96IWqEnede4nbiv9mvu07H/Ws37DSTXGJlN1l1m15cdTAwMTJGV/dyfbfjXHUwMDA2SVxmV/87/FxcKv2efs1JXHUwMDE3uY3EXHTOfDc9IZ3KXHUwMDA0JJjxyeHdMEilpVJcdCOVXHUwMDE5zXvxXHUwMDA23C5xmzDZXHUwMDAykd1sxlx1MDAwZa3UX56u8lajfr6zj/VvXHUwMDAz1yHRRT+7a8vz/cNk6KdSNaIwjlfbTtJoZ0fESVx1MDAxNJ67Na+ZtD8vXm58dG5cdTAwMWPCQmRnRWH/rFx1MDAxZLixXVx1MDAwMzJcdTAwMWFccrtOw0uG6TPi0ejVQrwoZSOX9k6UIcxcZlWGXHUwMDFhZrRQajRtLyAl0lpcdTAwMWGuXHUwMDE55lRzbiZcdTAwMDQrhz68XHUwMDBlXHUwMDEw7ClOP5lodadxflx1MDAwNvJcdTAwMDXN0TFJ5Fx1MDAwNHHXieClZcddXFw/slx1MDAwMEGYMEJJw5RmLJOj7Xpn7Vx1MDAwNFx1MDAwZWFcdTAwMTIjTbDhXFwyJoimMpPGTV9cclx1MDAxMVxcK1xml8jOtjJ0t5opJb9mLyRcdTAwMDK+tuwpQd/38+tcdTAwMTk0r9dzbKJuJzZzwGWX6nebzlx1MDAxNVx1MDAxOERcdTAwMTEmKeZSgJSjed9cdTAwMGLOJy/nh43zjKV09MOzWzCsjChCmGCl4aVcbknmhtjpVVx1MDAwNqe7XHUwMDA3bqvSOKqel/1cdTAwMWHFXHUwMDFi61x1MDAwNVx1MDAxME+AuDh8OdJcdTAwMDLAoFx1MDAxNFOjXGah4/hqhGFcdTAwMWSoxFx1MDAwMpBRWj9cdTAwMWO+xCBDODVcXGqBQZBpfKlGQlx0hTU2RmEpmZ7GXHUwMDE3c0OtXHUwMDFhfjP4ur7vdeNcdTAwMWLhhftcdTAwMTTBK7li5Gvs78nxK+dQXGYvejtcdTAwMDde69xcdTAwMDSV+JeXRfZ3Jrrk8dBlXHUwMDFjXHSARVx1MDAwMppEXHUwMDEzg8ctr5JAXHUwMDBio5JcdTAwMDHZXHUwMDE4kOJ3Qfdpy1x1MDAxMVTQaWxcdENcdTAwMWN0XHUwMDA3nFx1MDAxZOhcdTAwMGblWvNpblx0XHUwMDA1OSU3YHIxZYxyNcktXHUwMDAwXHUwMDBilvtbsrozsFU5XHUwMDFiM4Gt4kRcdTAwMWH4Oze2g+5cdTAwMWXpuoPN9ZbgZ69cdTAwMTmpXVbLtSXHVlx1MDAxMsSNXHUwMDExlFBcclx1MDAxNlx1MDAxNYtxalx1MDAwNThxooQ2XHUwMDE4XFwx0fKO1NYxXHUwMDE2XHUwMDBmRS1cdTAwMDSANtigmHxcdTAwMTfYSlWELVx1MDAwM3NLhNBsbm533uDt/dPN3v4gpju7XXZcdTAwMTJcdTAwMGZcdTAwMWG/LTm32iCwgYJDLKshvuTj3EJcdTAwMTiBOVx1MDAwNJZcdTAwMWNsMeNE3IlbQutcdTAwMTA1P1x1MDAxOLdKQKzOXHQ131xmt4l7mdxcdTAwMThcIlx1MDAxNEZcYlxcW88oclB/idmzzsn62/CiXHUwMDE19Fx1MDAwZvc3TpOeXGLeXHUwMDBivdzRLVx1MDAxN1x1MDAxNCmIXHIwY0bAs04kZ1xuI8CDXHUwMDE5XGJcdTAwMWQ0VzznfiehJa79c/voXHUwMDE2zDpEKIaBJJgqxm/glopJTjlIXHUwMDA1XGbrXHUwMDA1Yio1hORUf1x1MDAwNaaZVGGQXHUwMDFjeu/Talx1MDAwMFx1MDAxZVx1MDAxYq04XHUwMDFkz1x1MDAxZo691JTflOOo41x1MDAwNY6/Mjaz5ntnluZcdTAwMTXfbY1jnnhccsdcdTAwMWZNJ2E3m23AnVx1MDAxYy9wo+mVXHQj78ze5ajwrvCcbnVkUVDu3dSd2LWzdlxc30oniTCTo6OckzCqXHUwMDE1pGPzR0CXeu2UvD5cdTAwMWGcvCtv1JPGsLKvPLLcWkmZLYpcdTAwMTBuuMBgwlxyXHUwMDFlzzmpxogxQVx1MDAxNbf+RIviksldtZJcdTAwMTKMXGZT2cvNlDGXLVxcKSPF0qQ2YoGp5ZU2mjyl96iNWfTyWVx1MDAxYv9S6lx1MDAwZZN2XHUwMDE4lLzAXCKPusPH1ctZ95/U0Fx1MDAxYlx1MDAxNdTcUkHJ5OhIQVx1MDAwNbYhXHUwMDA1o/O7TYdcZumx13ldebllZCu52KItvrPkXG5KJVx1MDAwMlx1MDAxZsVcdTAwMDSXRFKFc4FtXG5cbjZIQZpcdTAwMDYpNzdE6clcdTAwMTg0U1DaMi7ndylcblx0JOlccvpJclx1MDAxMF8rKCNcdTAwMTCFa/pcdTAwMWSp56eP//70xz9cdTAwMTf59+N//lx1MDAxMXz6418lr9NccqOklLS9uDTzXHUwMDAzXHUwMDA3X53RXG6jktXrXHUwMDEyvP4z9yeCf37xhTPsp1x1MDAxYnlB8lPw81x1MDAxY/f4+L9Fr81/XHUwMDFm11j+oOHPQsNcXK6LiJm+a3ZnjnA5OTzyYJA9U1tcdTAwMWKe24GpzfiClVtcdTAwMGX1anHjpFLeXHUwMDFjen7052jNXHTEId9cdTAwMTKUgF9cdTAwMThrV1x1MDAwZdP0ykBgr4WEIFx1MDAxM1x1MDAxNlx1MDAwNcs71SxcdTAwMWWhN6chMIHogyy8uUHyKD9Yby5f0J/qzVx0SNgxJ/NT3H7/+vByUD1+e+iL9XpMnMBcdTAwMWK2lz1cZlOIci6UwMZcdTAwMDae48VcdTAwMGJu0yRcdTAwMDW8QDjKJeeYPVx1MDAxY7730puDlFx1MDAwZkIkyG+/XHUwMDE5fGdVi1x1MDAxNS6C1+a98FLN/H3lkzZda1x1MDAxZZQjUj1uXHUwMDFlXHUwMDFjNdpq66R6seTVYsjxXHJYPKZcdTAwMDVcdTAwMDXDy+SE7dVcdTAwMWFcdTAwMDG6WsExmuW7zsvXnKPEdpRccv4+wNW0cFOPzfkgIzTztzmq+28q4dbh+erQK7d5r856tfLakoMrXHUwMDE50jaxXHUwMDA150JcdTAwMTlcdTAwMTZ40uoqXHUwMDA040yJtHt3p/1cdTAwMTBcdTAwMGbcnlx1MDAwM72TWkqyyIrVI3Kriquq1LZ7zNdY3Jc7nlx1MDAxYuNcdTAwMWVf9VuDau3dpnpVq95qJ88jgmtcYjhpKYhSVLHc1pDP2Fx1MDAxMiWxpFx1MDAxMGOSO8Zcblx1MDAwZtye00pcdI5cdTAwMDVZeFf53rAtLDTqwiCBMNugs3ZobmYvutH+5j7ZXHUwMDEwu1x1MDAxYvuy2Ze7ZH/TXe5cYpdcdTAwMGKBIOfBXG645Fx1MDAxOPLSySiBXCJbZSRcdTAwMTAvSVx1MDAwNS+keFx1MDAwZs9CXHUwMDFhdFx1MDAwNljHkK0tvEH3UDXH77NBR1Vx/Z/BmlOt8fyJ53FP7jjbtdO1zdW9gyZdq78/7peXWy0h3kVUUS5cdTAwMTUziimWlVwi0j2hhlwiSPFsRFxm5Fx1MDAxOVpcXP5/zP6cLVx0gFx1MDAwMqtcdTAwMWb9ueyYb7Q/XHUwMDA3IXqhfnJqgcFfUVx1MDAxOPLXvM6GqO329ereK1V/XelttypLrp9UI1xi8Vx1MDAxOIUn5ZBCT2zFolx1MDAxNFx1MDAxMckoXHUwMDEznCqueXGs96jtOaGxXmhQl2on+5otV3fTzlx1MDAxZlxymVx1MDAxOWcsVUPmR3vuXHUwMDA3XHKP2Z6jhWmfpmCoXHUwMDE4IfOX2EhvfbuqlcvLb99eXjr97kbz6HLJ3Vx1MDAxN+OIQV5cdTAwMDc+Styw/1x1MDAwYuJNJLFcdTAwMDT/JpWcVVx1MDAxYXYxI4zMdF9PW62GaZhcdTAwMWJKXHUwMDE1XHUwMDE0XHQsXHUwMDA1xpRzwvGNXowypLUxIIfdlkem8z/wK5xA1irvIVx1MDAwMfyMUFx1MDAwNlx1MDAxMbtcdTAwMWX5UOzsRudkZ4/AONi+PN9cdTAwMWVe+q3tpNojvai25Veyus5cdTAwMTitTlx1MDAxNIVcdTAwMTcro5lcdTAwMGbX381ypZLl+6VcdTAwMGbZXHUwMDA2pKSwIM1cdTAwMTTmkI3kXHUwMDA0+ZK2bPT97aNcck81XHUwMDBl+/XtNVapbe3t9e5VW5phYm9/j+pcdTAwMDKJXHUwMDE4YkZAslx1MDAwNeGTytec7flcdTAwMWFzZCRcdTAwMTd2+5PUbEa0t3h1XHUwMDExttZjf1liQeryaFx1MDAwNb8rhbpcdTAwMDFmZoo3Zlx1MDAxMG53vOr5XHUwMDBifrM1fFx1MDAwMaZffVx1MDAxMWWGXHUwMDExNlxubD9k6oTh8X2Fmk7sx79LYaFcdTAwMTBlrjRcdTAwMDKGpaKQjlxiIFlOo6xcdTAwMDRcYmJ/aVBcdTAwMThObJljXG5larRS1NxH6W9cdTAwMWFlelx1MDAxN5RT26xulebEiVx1MDAxMyXrXtD0grPJU9ygmc3kRL7+T1x1MDAwNLbmXGJG0pSp0bfyr2LEIVFnRHNJMDNgMXjusDOna99cdTAwMTSCjI1rMFx1MDAxOFQoa1CmlsV34qRcdTAwMWN2Op61ub+EXHUwMDEwb06KnT7SmlXHtutMvVx1MDAwNnio/Nyk3nbtXHUwMDE1x1x1MDAxZG32XSljO/1h9P2vz248epVyg6RUXHUwMDAwN4RccpCW6/zpq1xuKWNgkCtpofzi1YoxvrrcJMHZXHUwMDA1n+T/vZ1LxoVcdTAwMDVcdTAwMTj7aNpQOn9cdTAwMDB7vMferVV3XHUwMDFijvP2/M3uprOxeeRcdTAwMWQtd1x1MDAwMKvA1UlcYl+lxvC4dLw8XG4mXHUwMDFlaSo45pxSlf8ls6VzyFx1MDAwNixcdTAwMTiwI+6hZHrP/lhqXHUwMDEwLVx1MDAxNyfM64+fXFwrzIrT7Vx1MDAxZSZwyZFo8Ghe87qGk11mZeC5XHUwMDE36zeuu/3YmDiV32Lops/54cmH/1x1MDAwM7lI91xmIn0= terminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256fterminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256f

    There is an additional consideration that comes in to play when the output has less lines than the previous frame. If we were to write a shorter frame, it wouldn't fully overwrite the previous frame. We would be left with a few lines of a previous frame that wouldn't update.

    The solution to this problem is to write an escape code that clears lines from the cursor downwards before we write a smaller frame. You can see this in action in the above video. The inline app can grow or shrink in size, and still be anchored to the bottom of the terminal.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#cursor-input","title":"Cursor input","text":"

    The cursor tells the terminal where any text will be written by the app, but it also assumes this will be where the user enters text. If you enter CJK (Chinese Japanese Korean) text in to the terminal, you will typically see a floating control that points where new text will be written. If you are on a Mac, the emoji entry dialog (Ctrl+Cmd+Space) will also point at the current cursor position. To make this work in a sane way, we need to move the terminal's cursor to where any new text will appear.

    The following diagram shows the cursor moving to the point where new text is displayed.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2a627bRlx1MDAxNoD/5ylcdTAwMDRlf7RAzMzMmWuAxcJ27Nhx41xc3NSut0VAkyOJNUWyJGVbKVxmXHUwMDE0fYbmMbq7z5Qn2TO0I0qUZKuOb73Qhi3O9fDMd+acM9RPXHUwMDBmWq12Ocxs+0mrbU9cdTAwMDI/jsLcP24/cuVHNi+iNMEqVt1cdTAwMTfpIFx1MDAwZqqWvbLMiiePXHUwMDFm9/380JZZ7Fx1MDAwN9Y7ioqBXHUwMDFmXHUwMDE35SCMUi9I+4+j0vaLf7m/237f/jNL+2GZe/UkSzaMyjQ/m8vGtm+TssDR/433rdZP1d8x6XJcdTAwMWKUftKNbdWhqqpcdTAwMDWkjJlm8XaaVNJSobk2Wkkxalx1MDAxMVx1MDAxNU9xwtKGWN1BoW1d44rar9+Q7GiLraytL29mS/L5yTc/RLv1vJ0ojnfKYVxcyVx1MDAxNeRpUSz1/DLo1S2KMk9cdTAwMGbtblx1MDAxNJa9T+pcdTAwMWIrXHUwMDFm9S1SVEXdK09cdTAwMDfdXmJcdTAwMGKnXHUwMDA1OipNMz+IyqErI2RUeqaKJ6265Fx1MDAwNO+48oBcdTAwMTFGqVx1MDAwMEo0UC5H1WdcdTAwMDNIT3AlJZdMXHRcdTAwMDGsKdlqXHUwMDFh44qgZFx1MDAwZkl11bJcdTAwMWT4wWFcdTAwMTdcdTAwMDVMwlGbMveTXCLzc1xct7rd8fkzXHUwMDBiXHUwMDA2XHUwMDFlXGIjlDSgNIBcdTAwMWG16Nmo2yuxXHRI4mlKXGbnXHUwMDEyQFDNamFcdTAwMGJbrY0mRoBSXHUwMDEyRlx1MDAxNU6EbDOsOPm+XpBcdTAwMWNcdNt0PZJBXHUwMDFjj+szXHTP9TlRceAq1saQq4dcdTAwMWFkoX9cdTAwMDZcdTAwMDZVXHUwMDE0JJOaaSlquOIoOWxcdTAwMGVcdTAwMTenwWHNUlV6+uhcblx1MDAxNFx1MDAxYkPmQqy0MIpcdTAwMThtXHUwMDE2hvjHr7rrq2+Wj9f2uqTc3szWNlx1MDAwM39jXHUwMDBlxFxyXHUwMDEw71xmX+NR4Fx1MDAwMjjnhCjJTYNe5UmpXHUwMDA1Z6CMJOPV104vNZ6hnFx1MDAxOY7zXHUwMDExZeg0vUx7Qlx0RZBRXFxcdTAwMTgpQTfpXHUwMDE1XHUwMDFjgKEhij9ccr02jqOsmMmuXHUwMDE2MI9dzXEtJfDF0e2+j168XHUwMDEw2TqYcqhXzP7zb/rv315cdTAwMDVdemvoXG7taW64UVpqQyVcdTAwMTI8yS6VXHUwMDFlXHUwMDAzMMo4KnBTa0r2u9h92PFcdTAwMDVcdTAwMTNsmltcblx1MDAxZSeMXHQj0Vx1MDAwYjCuNZ9cdTAwMDaXMk+gXHUwMDA3MLjlXHUwMDEyXHUwMDE0iXHVXHUwMDA01/UlnCui/lxu5Jqxx2ySXHUwMDBiwJkyv4Pc7bWXXHUwMDFi5bF+2SGr79ZcdTAwMGZiXHUwMDE28/f7nftNrlx1MDAxNp7SXHUwMDA0d13BiFZKq1x1MDAwNrjCI1x1MDAxNCFcdTAwMTGMaSXImJlfXHLcXHUwMDAzQsRNgUu5llxcMKn+XHUwMDEyWy7qalx1MDAxZbhC4V4jcVFcdTAwMTdcdTAwMDZ36+nhXHUwMDBifTLs7m1nvb2NV1svI1x1MDAwMUf3XHUwMDFiXFyK0GhcZlx1MDAwM1x1MDAxNG5Uwlx1MDAwMLrpXHUwMDA2udzjUtAq1EVi9GdcdTAwMDW7XHUwMDBmKTvQWt5cdTAwMTi5Qlx1MDAxYuCamj9cdTAwMGa5pT0pZ2HL5NxIgUpCccVAwcLc/rD79sSHr0Gvvn6z+3SfLNFnavN+R7lMSc9grFx1MDAwZkJcdTAwMDKT+LyNUIFg7Ek0brycUMVByrncUut+rlx1MDAxZeYqgVx0mDGYK1x1MDAwMmFcbvhcZnSZmEJVXHUwMDAyZcDUWFxi81x1MDAwN0C1lipNyp3ova3inInSdb9cdTAwMWbFw4llrVx1MDAxOK5YzvtR4sftiZrlOOo6otux7UyiXkaBXHUwMDFmj6rLNKtrXHUwMDAznMmPXHUwMDEym09rJs2jrpvl67mz4nPajdGu4o0tzoFfWFfryvWV7Fx1MDAxMoRolo7sXHUwMDEyWUVcdTAwMWUx3VnYLjl59tan20d+9JzRaOdN9mpJsfttl5J66Cw4hlx1MDAwZlxcYOJcdTAwMDeT7lx1MDAwNFxcSmhcdTAwMDTarZIuir85q2SUeFx1MDAwNsZimNpcdTAwMThrXHUwMDAz/WSMXG4loZzDXHUwMDFk+1xyReg4pNdojLUr+GSM/2hlw7KXJq0occR72fB2zfKi+ZtcdTAwMDY60z7N1eyTz81ThDuA0Fx1MDAxNOo1uMw8i63lYfx8XHUwMDA11nskXGJ3XHUwMDBls414S2T32zxcdTAwMDXxJCa9mF9cdTAwMGJcdTAwMDBkrlx1MDAxZaXChFx1MDAxYlx1MDAwZkP/KrdmnEvdkKs2T9YxlvPPOVx1MDAxYlx1MDAxMk6QWVFeLfDIPCWG4VxmQ9C7Nk82juiNmufHXHUwMDBmv3389ee7/P3wn++Sj7/+0or6WZqXrbJcdTAwMTdcdTAwMTWtXHUwMDBiL2x81qOT5i1n1y1cdTAwMDSga7+g5Msnl/RwV5ZHSflF8uVcdTAwMDJzfPjfXevmv7e7Wf5Nw1x1MDAxZoWGhVxcXHUwMDE3XHUwMDE1XHUwMDE3+q5cdTAwMGJfb1x1MDAwMJ3rwDDrk1x1MDAxMjd1tXiA2Vx1MDAwYsPO9ru9XHUwMDFmXHUwMDA3sYSlwVx0W9/v7r+731x1MDAxZYxhXGJcdIxi2qdcdTAwMThRXHUwMDA006hcdFx1MDAxN8ZcdTAwMTnxmCGcXGKXi4HiXHLBalx1MDAxN2axL9BLTto6gVx0zIzzXG7mXHRcIlx1MDAwNSHoJSknMz1cdTAwMTlcdTAwMDNPa1xmdkFSYJrOyFx1MDAwMYlcdTAwMDDsj20+3699gqjGXGLOS07nu7tRn7r3iIxju9594dM1sqo3l5dcdTAwMDa7+1n323ykiFx0Xv08T4/bo5rT80/343Ugp3PfaVx1MDAwM/IhheKLXHUwMDA3fGHk2+Dbbnm4WiztvVx1MDAxYa7oZ6+D19dqLmFauumvM+IznsRcXEtzQrTShky+zMbVQGvSXFxcdTAwMTJpwL1qvsfmXCJcdTAwMTU+giHmXHUwMDFhjkyuZC63XHUwMDA29JlBzYDZSD5cdTAwMGZmyo3WinJYfPO/2MTvYPNXl1x1MDAxZS5cYkxP8Fx1MDAxMSWbtfWDwuyFUFx1MDAwNVx1MDAwMIg7tvqcw4X5LFx1MDAwYuqhsinRWktFZ1x1MDAxZVVjloXJXHUwMDAyo0BcYroppae+k0E1+ih5Uyyz22V5TM1+Xq5ESVx1MDAxOCXdZlx1MDAxN5uEdc2YyOffWdpcXCBcdTAwMWOpsqZg4OQnuFx1MDAwNFxmd1x01LJcdTAwMTRcdTAwMTRcdTAwMTVdXHUwMDFmqjlV+pnTgoerQ1D51X5CzbSFx35Rrqb9fuT23FcpRpxNqasnWnbm2LP+1CrgM43XNe02cyNOOtr6U6tGu7pcdTAwMTl9/v7RzNZKeSCldF9cdTAwMDKQXHUwMDA0o5/xzlxcee49s+S0qmLissHmM+yuKXrr4Vx1MDAxZYz/d46+mqDtZ9lOiVx1MDAxNI2WXHUwMDE2aY7C86S3Vln7KLLHKzNtzV0uhKhcdTAwMTbHbT22Qvv0wen/XHUwMDAxc82RiyJ9 terminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256f

    This only really impacts text entry (such as the Input and TextArea widgets).

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#mouse-control","title":"Mouse control","text":"

    Inline apps in Textual support mouse input, which works the same as fullscreen apps.

    To use the mouse in the terminal you send an escape code which tells the terminal to write encoded mouse coordinates to standard input. The mouse coordinates can then be parsed in much the same was as reading keys.

    In inline mode this works in a similar way, with an added complication that the mouse origin is at the top left of the terminal. In other words if you move the mouse to the top left of the terminal you get coordinate (0, 0), but the app expects (0, 0) to be where it was displayed.

    In order for the app to know where the mouse is relative to it's origin, we need to ask the terminal where the cursor is. We do this with an escape code, which tells the terminal to write the current cursor coordinate to standard input. We can then subtract that coordinate from the physical mouse coordinates, so we can send the app mouse events relative to its on-screen origin.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#tldr","title":"tl;dr","text":"

    Escapes codes.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#found-this-interesting","title":"Found this interesting?","text":"

    If you are interested in Textual, join our Discord server.

    Or follow me for more terminal shenanigans.

    • @willmcgugan
    • mastodon.social/@willmcgugan
    "},{"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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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/2024/02/20/remote-memory-profiling-with-memray/","title":"Remote memory profiling with Memray","text":"

    Memray is a memory profiler for Python, built by some very smart devs at Bloomberg. It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!

    They recently added a Textual interface which looks amazing, and lets you monitor your process right from the terminal:

    You would typically run this locally, or over a ssh session, but it is also possible to serve the interface over the web with the help of textual-web. I'm not sure if even the Memray devs themselves are aware of this, but here's how.

    First install Textual web (ideally with pipx) alongside Memray:

    pipx install textual-web\n

    Now you can serve Memray with the following command (replace the text in quotes with your Memray options):

    textual-web -r \"memray run --live -m http.server\"\n

    This will return a URL you can use to access the Memray app from anywhere. Here's a quick video of that in action:

    "},{"location":"blog/2024/02/20/remote-memory-profiling-with-memray/#found-this-interesting","title":"Found this interesting?","text":"

    Join our Discord server if you want to discuss this post with the Textual devs or 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/2024/09/08/towards-textual-web-applications/","title":"Towards Textual Web Applications","text":"

    In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#what-is-textual-serve","title":"What is textual-serve?","text":"

    textual-serve is an open source project which allows you to serve and access your Textual app via a browser. The Textual app runs on a machine/server under your control, and communicates with the browser via a protocol which runs over websocket. End-users interacting with the app via their browser do not have access to the machine the application is running on via their browser, only the running Textual app.

    For example, you could install harlequin (a terminal-based SQL IDE) on a machine on your network, run it using textual-serve, and then share the URL with others. Anyone with the URL would then be able to use harlequin to query databases accessible from that server. Or, you could deploy posting (a terminal-based API client) on a server, and provide your colleagues with the URL, allowing them to quickly send HTTP requests from that server, right from within their browser.

    Accessing an instance of Posting via a web browser."},{"location":"blog/2024/09/08/towards-textual-web-applications/#providing-an-equal-experience","title":"Providing an equal experience","text":"

    While you're interacting with the Textual app using your web browser, it's not running in your browser. It's running on the machine you've installed it on, similar to typical server driven web app. This creates some interesting challenges for us if we want to provide an equal experience across browser and terminal.

    A Textual app running in the browser is inherently more accessible to non-technical users, and we don't want to limit access to important functionality for those users. We also don't want Textual app developers to have to repeatedly check \"is the the end-user using a browser or a terminal?\".

    To solve this, we've created APIs which allow developers to add web links to their apps and deliver files to end-users in a platform agnostic way. The goal of these APIs is to allow developers to write applications knowing that they'll provide a sensible user experience in both terminals and web browsers without any extra effort.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#opening-web-links","title":"Opening web links","text":"

    The ability to click on and open links is a pretty fundamental expectation when interacting with an app running in your browser.

    Python offers a webbrowser module which allows you to open a URL in a web browser. When a Textual app is running in a terminal, a simple call to this module does exactly what we'd expect.

    If the app is being used via a browser however, the webbrowser module would attempt to open the browser on the machine the app is being served from. This is clearly not very useful to the end-user!

    To solve this, we've added a new method to Textual: App.open_url. When running in the terminal, this will use webbrowser to open the URL as you would expect.

    When the Textual app is being served and used via the browser however, the running app will inform textual-serve, which will in turn tell the browser via websocket that the end-user is requesting to open a link, which will then be opened in their browser - just like a normal web link.

    The developer doesn't need to think about where their application might be running. By using open_url, Textual will ensure that end-users get the experience they expect.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#saving-files-to-disk","title":"Saving files to disk","text":"

    When running a Textual app in the terminal, getting a file into the hands of the end user is relatively simple - you could just write it to disk and notify them of the location, or perhaps open their $EDITOR with the content loaded into it. Given they're using a terminal, we can also make an assumption that the end-user is at least some technical knowledge.

    Run that same app in the browser however, and we have a problem. If you simply write the file to disk, the end-user would need to be able to access the machine the app is running on and navigate the file system in order to retrieve it. This may not be possible: they may not be permitted to access the machine, or they simply may not know how!

    The new App.deliver_text and App.deliver_binary methods are designed to let developers get files into the hands of end users, regardless of whether the app is being accessed via the browser or a terminal.

    When accessing a Textual app using a terminal, these methods will write a file to disk, and notify the App when the write is complete.

    In the browser, however, a download will be initiated and the file will be streamed via an ephemeral (one-time) download URL from the server that the Textual app is running on to the end-user's browser. If the app developer wishes, they can specify a custom file name, MIME type, and even whether the browser should attempt to open the file in a new tab or be downloaded.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#how-it-works","title":"How it works","text":"

    Input in Textual apps is handled, at the lowest level, by \"driver\" classes. We have different drivers for Linux and Windows, and also one for handling apps being served via web.

    When running in a terminal, the Windows/Linux drivers will read stdin, and parse incoming ANSI escape sequences sent by the terminal emulator as a result of mouse movement or keyboard interaction. The driver translates these escape sequences into Textual \"Events\", which are sent on to your application's message queue for asynchronous handling.

    For apps being served over the web, things are again a bit more complex. Interaction between the application and the end-user happens inside the browser - with a terminal rendered using xterm.js - the same front-end terminal engine used in VS Code. xterm.js fills the roll of a terminal emulator here, translating user interactions into ANSI escape codes on stdin.

    These escape codes are sent through websocket to textual-serve and then piped to the stdin stream of the Textual app which is running as a subprocess. Inside the Textual app, these can be processed and converted into events as normal by Textual's web driver.

    A Textual app also writes to the stdout stream, which is then read by your emulator and translated into visual output. When running on the web, this stdout is also sent over websocket to the end-user's browser, and xterm.js takes care of rendering.

    Although most of the data flowing back and forth from browser to Textual app is going to be ANSI escape sequences, we can in reality send anything we wish.

    To support file delivery we updated our protocol to allow applications to signal that a file is \"ready\" for delivery when one of the new \"deliver file\" APIs is called. An ephemeral, single-use, download link is then generated and sent to the browser via websocket. The front-end of textual-serve opens this URL and the file is streamed to the browser.

    This streaming process involves continuous delivery of encoded chunks of the file (using a variation of Bencode - the encoding used by BitTorrent) from the Textual app process to textual-serve, and then through to the end-user via the download URL.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#the-result","title":"The result","text":"

    These new APIs close an important feature gap and give developers the option to build apps that can accessed via terminals or browsers without worrying that those on the web might miss out on important functionality!

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#found-this-interesting","title":"Found this interesting?","text":"

    Join our Discord server to chat to myself and other Textual developers.

    "},{"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":"blog/2024/02/11/file-magic-with-the-python-standard-library/","title":"File magic with the Python standard library","text":"

    I recently published Toolong, an app for viewing log files. There were some interesting technical challenges in building Toolong that I'd like to cover in this post.

    Python is awesome

    This isn't specifically Textual related. These techniques could be employed in any Python project.

    These techniques aren't difficult, and shouldn't be beyond anyone with an intermediate understanding of Python. They are the kind of \"if you know it you know it\" knowledge that you may not need often, but can make a massive difference when you do!

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#opening-large-files","title":"Opening large files","text":"

    If you were to open a very large text file (multiple gigabyte in size) in an editor, you will almost certainly find that it takes a while. You may also find that it doesn't load at all because you don't have enough memory, or it disables features like syntax highlighting.

    This is because most app will do something analogous to this:

    with open(\"access.log\", \"rb\") as log_file:\n    log_data = log_file.read()\n

    All the data is read in to memory, where it can be easily processed. This is fine for most files of a reasonable size, but when you get in to the gigabyte territory the read and any additional processing will start to use a significant amount of time and memory.

    Yet Toolong can open a file of any size in a second or so, with syntax highlighting. It can do this because it doesn't need to read the entire log file in to memory. Toolong opens a file and reads only the portion of it required to display whatever is on screen at that moment. When you scroll around the log file, Toolong reads the data off disk as required -- fast enough that you may never even notice it.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#scanning-lines","title":"Scanning lines","text":"

    There is an additional bit of work that Toolong has to do up front in order to show the file. If you open a large file you may see a progress bar and a message about \"scanning\".

    Toolong needs to know where every line starts and ends in a log file, so it can display a scrollbar bar and allow the user to navigate lines in the file. In other words it needs to know the offset of every new line (\\n) character within the file.

    This isn't a hard problem in itself. You might have imagined a loop that reads a chunk at a time and searches for new lines characters. And that would likely have worked just fine, but there is a bit of magic in the Python standard library that can speed that up.

    The mmap module is a real gem for this kind of thing. A memory mapped file is an OS-level construct that appears to load a file instantaneously. In Python you get an object which behaves like a bytearray, but loads data from disk when it is accessed. The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actual reading of the file to the OS.

    Here's the method that Toolong uses to scan for line breaks. Forgive the micro-optimizations, I was going for raw execution speed here.

        def scan_line_breaks(\n        self, batch_time: float = 0.25\n    ) -> Iterable[tuple[int, list[int]]]:\n        \"\"\"Scan the file for line breaks.\n\n        Args:\n            batch_time: Time to group the batches.\n\n        Returns:\n            An iterable of tuples, containing the scan position and a list of offsets of new lines.\n        \"\"\"\n        fileno = self.fileno\n        size = self.size\n        if not size:\n            return\n        log_mmap = mmap.mmap(fileno, size, prot=mmap.PROT_READ)\n        rfind = log_mmap.rfind\n        position = size\n        batch: list[int] = []\n        append = batch.append\n        get_length = batch.__len__\n        monotonic = time.monotonic\n        break_time = monotonic()\n\n        while (position := rfind(b\"\\n\", 0, position)) != -1:\n            append(position)\n            if get_length() % 1000 == 0 and monotonic() - break_time > batch_time:\n                break_time = monotonic()\n                yield (position, batch)\n                batch = []\n                append = batch.append\n        yield (0, batch)\n        log_mmap.close()\n

    This code runs in a thread (actually a worker), and will generate line breaks in batches. Without batching, it risks slowing down the UI with millions of rapid events.

    It's fast because most of the work is done in rfind, which runs at C speed, while the OS reads from the disk.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#watching-a-file-for-changes","title":"Watching a file for changes","text":"

    Toolong can tail files in realtime. When something appends to the file, it will be read and displayed virtually instantly. How is this done?

    You can easily poll a file for changes, by periodically querying the size or timestamp of a file until it changes. The downside of this is that you don't get notified immediately if a file changes between polls. You could poll at a very fast rate, but if you were to do that you would end up burning a lot of CPU for no good reason.

    There is a very good solution for this in the standard library. The selectors module is typically used for working with sockets (network data), but can also work with files (at least on macOS and Linux).

    Software developers are an unimaginative bunch when it comes to naming things

    Not to be confused with CSS selectors!

    The selectors module can tell you precisely when a file can be read. It can do this very efficiently, because it relies on the OS to tell us when a file can be read, and doesn't need to poll.

    You register a file with a Selector object, then call select() which returns as soon as there is new data available for reading.

    See watcher.py in Toolong, which runs a thread to monitors files for changes with a selector.

    Addendum

    So it turns out that watching regular files for changes with selectors only works with KqueueSelector which is the default on macOS. Disappointingly, the Python docs aren't clear on this. Toolong will use a polling approach where this selector is unavailable.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#textual-learnings","title":"Textual learnings","text":"

    This project was a chance for me to \"dogfood\" Textual. Other Textual devs have build some cool projects (Trogon and Frogmouth), but before Toolong I had only ever written example apps for docs.

    I paid particular attention to Textual error messages when working on Toolong, and improved many of them in Textual. Much of what I improved were general programming errors, and not Textual errors per se. For instance, if you forget to call super() on a widget constructor, Textual used to give a fairly cryptic error. It's a fairly common gotcha, even for experience devs, but now Textual will detect that and tell you how to fix it.

    There's a lot of other improvements which I thought about when working on this app. Mostly quality of life features that will make implementing some features more intuitive. Keep an eye out for those in the next few weeks.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#found-this-interesting","title":"Found this interesting?","text":"

    If you would like to talk about this post or anything Textual related, join us on the 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/hatch/","title":"<hatch>","text":"

    The <hatch> CSS type represents a character used in the hatch rule.

    "},{"location":"css_types/hatch/#syntax","title":"Syntax","text":"Value Description cross A diagonal crossed line. horizontal A horizontal line. left A left leaning diagonal line. right A right leaning diagonal line. vertical A vertical line."},{"location":"css_types/hatch/#examples","title":"Examples","text":""},{"location":"css_types/hatch/#css","title":"CSS","text":"
    .some-class {\n    hatch: cross green;\n}\n
    "},{"location":"css_types/hatch/#python","title":"Python","text":"
    widget.styles.hatch = (\"cross\", \"red\")\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 click (top left).

    "},{"location":"events/app_blur/","title":"AppBlur","text":"

    Bases: Event

    Sent when the app loses focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusOut, or when running via textual-web.

    "},{"location":"events/app_blur/#see-also","title":"See also","text":"
    • AppFocus
    "},{"location":"events/app_focus/","title":"AppFocus","text":"

    Bases: Event

    Sent when the app has focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusIn, or when running via textual-web.

    "},{"location":"events/app_focus/#see-also","title":"See also","text":"
    • AppBlur
    "},{"location":"events/blur/","title":"Blur","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":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/click/#see-also","title":"See also","text":"
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/descendant_blur/","title":"DescendantBlur","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\n

    The widget that was blurred (alias of widget).

    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was blurred.

    "},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantFocus
    • Focus
    "},{"location":"events/descendant_focus/","title":"DescendantFocus","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\n

    The widget that was focused (alias of widget).

    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was focused.

    "},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantBlur
    • Focus
    "},{"location":"events/enter/","title":"Enter","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    Note that this event bubbles, so a widget may receive this event when the mouse moves over a child widget. Check the node attribute for the widget directly under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"events/enter/#textual.events.Enter.node","title":"node instance-attribute","text":"
    node = node\n

    The node directly under the mouse.

    "},{"location":"events/enter/#see-also","title":"See also","text":"
    • Click
    • Leave
    • MouseDown
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/focus/","title":"Focus","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantBlur
    • DescendantFocus
    "},{"location":"events/hide/","title":"Hide","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    Sent when any of the following conditions apply:

    • The widget is removed from the DOM.
    • The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
    • The widget has its display attribute set to False.
    • The widget's display style is set to \"none\".
    "},{"location":"events/key/","title":"Key","text":"

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    The key that was pressed.

    required str | None

    A printable character or None if it is not printable.

    required"},{"location":"events/key/#textual.events.Key(key)","title":"key","text":""},{"location":"events/key/#textual.events.Key(character)","title":"character","text":""},{"location":"events/key/#textual.events.Key.aliases","title":"aliases instance-attribute","text":"
    aliases = _get_key_aliases(key)\n

    The aliases for the key, including the key itself.

    "},{"location":"events/key/#textual.events.Key.character","title":"character instance-attribute","text":"
    character = (\n    key\n    if len(key) == 1\n    else None if character is None else character\n)\n

    A printable character or None if it is not printable.

    "},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable\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.key","title":"key instance-attribute","text":"
    key = key\n

    The key that was pressed.

    "},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
    name\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\n

    The corresponding name for every alias in aliases list.

    "},{"location":"events/leave/","title":"Leave","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget, or if a widget is programmatically disabled while hovered.

    Note that this widget bubbles, so a widget may receive Leave events for any child widgets. Check the node parameter for the original widget that was previously under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"events/leave/#textual.events.Leave.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was previously directly under the mouse.

    "},{"location":"events/leave/#see-also","title":"See also","text":"
    • Click
    • Enter
    • MouseDown
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/load/","title":"Load","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"events/load/#see-also","title":"See also","text":"
    • Mount
    "},{"location":"events/mount/","title":"Mount","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/#see-also","title":"See also","text":"
    • Load
    • Unmount
    "},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

    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:

    Name Type Description Default Offset

    The position of the mouse when captured.

    required"},{"location":"events/mouse_capture/#textual.events.MouseCapture(mouse_position)","title":"mouse_position","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when captured.

    "},{"location":"events/mouse_capture/#see-also","title":"See also","text":"
    • capture_mouse
    • release_mouse
    • MouseRelease
    "},{"location":"events/mouse_down/","title":"MouseDown","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_down/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_move/","title":"MouseMove","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_move/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_release/","title":"MouseRelease","text":"

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Offset

    The position of the mouse when released.

    required"},{"location":"events/mouse_release/#textual.events.MouseRelease(mouse_position)","title":"mouse_position","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when released.

    "},{"location":"events/mouse_release/#see-also","title":"See also","text":"
    • capture_mouse
    • release_mouse
    • MouseCapture
    "},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_scroll_down/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_scroll_up/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseUp
    "},{"location":"events/mouse_up/","title":"MouseUp","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_up/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    "},{"location":"events/paste/","title":"Paste","text":"

    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:

    Name Type Description Default str

    The text that has been pasted.

    required"},{"location":"events/paste/#textual.events.Paste(text)","title":"text","text":""},{"location":"events/paste/#textual.events.Paste.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was pasted.

    "},{"location":"events/print/","title":"Print","text":"

    Bases: Event

    Sent to a widget that is capturing print.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    Text that was printed.

    required bool

    True if the print was to stderr, or False for stdout.

    False Note

    Python's print output can be captured with App.begin_capture_print.

    "},{"location":"events/print/#textual.events.Print(text)","title":"text","text":""},{"location":"events/print/#textual.events.Print(stderr)","title":"stderr","text":""},{"location":"events/print/#textual.events.Print.stderr","title":"stderr instance-attribute","text":"
    stderr = stderr\n

    True if the print was to stderr, or False for stdout.

    "},{"location":"events/print/#textual.events.Print.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was printed.

    "},{"location":"events/resize/","title":"Resize","text":"

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Size

    The new size of the Widget.

    required Size

    The virtual size (scrollable size) of the Widget.

    required Size | None

    The size of the Widget's container widget.

    None"},{"location":"events/resize/#textual.events.Resize(size)","title":"size","text":""},{"location":"events/resize/#textual.events.Resize(virtual_size)","title":"virtual_size","text":""},{"location":"events/resize/#textual.events.Resize(container_size)","title":"container_size","text":""},{"location":"events/resize/#textual.events.Resize.container_size","title":"container_size instance-attribute","text":"
    container_size = (\n    size if container_size is None else container_size\n)\n

    The size of the Widget's container widget.

    "},{"location":"events/resize/#textual.events.Resize.size","title":"size instance-attribute","text":"
    size = size\n

    The new size of the Widget.

    "},{"location":"events/resize/#textual.events.Resize.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size = virtual_size\n

    The virtual size (scrollable size) of the Widget.

    "},{"location":"events/screen_resume/","title":"ScreenResume","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_resume/#see-also","title":"See also","text":"
    • ScreenSuspend
    "},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/#see-also","title":"See also","text":"
    • ScreenResume
    "},{"location":"events/show/","title":"Show","text":"

    Bases: Event

    Sent when a widget is first displayed.

    • Bubbles
    • Verbose
    "},{"location":"events/unmount/","title":"Unmount","text":"

    Bases: Event

    Sent when a widget is unmounted and may no longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/unmount/#see-also","title":"See also","text":"
    • Mount
    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    "},{"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. Consider the following widget class:

    from textual.widgets import Static\n\nclass Alert(Static):\n    pass\n

    Alert widgets may be styled with the following CSS (to give them a red border):

    Alert {\n  border: solid red;\n}\n

    The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Alert (Python) class extends Static.

    Static {\n  background: blue;\n  border: rounded green;\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 Alert. When this happens, Textual will use the most recently defined sub-class. So Alert wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, Alert widgets would have a \"solid red\" border and not a \"rounded green\" border.

    "},{"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:

    • :blur Matches widgets which do not have input focus.
    • :dark Matches widgets in dark mode (where App.dark == True).
    • :disabled Matches widgets which are in a disabled state.
    • :enabled Matches widgets which are in an enabled state.
    • :focus-within Matches widgets with a focused child widget.
    • :focus Matches widgets which have input focus.
    • :inline Matches widgets when the app is running in inline mode.
    • :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=app.set_background('red')]Red[/]\n[@click=app.set_background('green')]Green[/]\n[@click=app.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=app.set_background('red')]Red[/]\n[@click=app.set_background('green')]Green[/]\n[@click=app.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=app.set_background('cyan')]Cyan[/]\n[@click=app.set_background('magenta')]Magenta[/]\n[@click=app.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.
    • focused invokes actions on the currently focused widget (if there is one).

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

    "},{"location":"guide/actions/#dynamic-actions","title":"Dynamic actions","text":"

    Added in version 0.61.0

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

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

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

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

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

    actions06.pyactions06.tcssOutput actions06.py
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Placeholder\n\nPAGES_COUNT = 5\n\n\nclass PagesApp(App):\n    BINDINGS = [\n        (\"n\", \"next\", \"Next\"),\n        (\"p\", \"previous\", \"Previous\"),\n    ]\n\n    CSS_PATH = \"actions06.tcss\"\n\n    page_no = reactive(0)\n\n    def compose(self) -> ComposeResult:\n        with HorizontalScroll(id=\"page-container\"):\n            for page_no in range(PAGES_COUNT):\n                yield Placeholder(f\"Page {page_no}\", id=f\"page-{page_no}\")\n        yield Footer()\n\n    def action_next(self) -> None:\n        self.page_no += 1\n        self.refresh_bindings()  # (1)!\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def action_previous(self) -> None:\n        self.page_no -= 1\n        self.refresh_bindings()  # (2)!\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def check_action(\n        self, action: str, parameters: tuple[object, ...]\n    ) -> bool | None:  # (3)!\n        \"\"\"Check if an action may run.\"\"\"\n        if action == \"next\" and self.page_no == PAGES_COUNT - 1:\n            return False\n        if action == \"previous\" and self.page_no == 0:\n            return False\n        return True\n\n\nif __name__ == \"__main__\":\n    app = PagesApp()\n    app.run()\n
    1. Prompts the footer to refresh, if bindings change.
    2. Prompts the footer to refresh, if bindings change.
    3. Guards the actions from running and also what keys are displayed in the footer.
    actions06.tcss
    #page-container {\n    # This hides the scrollbar\n    scrollbar-size: 0 0;\n}\n

    PagesApp Page\u00a00 \u00a0n\u00a0Next\u00a0\u258f^p\u00a0palette

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

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

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

    actions07.pyactions06.tcssOutput actions06.py
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Placeholder\n\nPAGES_COUNT = 5\n\n\nclass PagesApp(App):\n    BINDINGS = [\n        (\"n\", \"next\", \"Next\"),\n        (\"p\", \"previous\", \"Previous\"),\n    ]\n\n    CSS_PATH = \"actions06.tcss\"\n\n    page_no = reactive(0, bindings=True)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        with HorizontalScroll(id=\"page-container\"):\n            for page_no in range(PAGES_COUNT):\n                yield Placeholder(f\"Page {page_no}\", id=f\"page-{page_no}\")\n        yield Footer()\n\n    def action_next(self) -> None:\n        self.page_no += 1\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def action_previous(self) -> None:\n        self.page_no -= 1\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:\n        \"\"\"Check if an action may run.\"\"\"\n        if action == \"next\" and self.page_no == PAGES_COUNT - 1:\n            return None  # (2)!\n        if action == \"previous\" and self.page_no == 0:\n            return None  # (3)!\n        return True\n\n\nif __name__ == \"__main__\":\n    app = PagesApp()\n    app.run()\n
    1. The bindings=True causes the footer to refresh when page_no changes.
    2. Returning None disables the key in the footer rather than hides it
    3. Returning None disables the key in the footer rather than hides it.
    actions06.tcss
    #page-container {\n    # This hides the scrollbar\n    scrollbar-size: 0 0;\n}\n

    PagesApp Page\u00a00 \u00a0n\u00a0Next\u00a0\u00a0p\u00a0Previous\u00a0\u258f^p\u00a0palette

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

    "},{"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_focus_next
    • action_focus_previous
    • action_focus
    • action_pop_screen
    • action_push_screen
    • action_quit
    • action_remove_class
    • action_screenshot
    • action_simulate_key
    • action_suspend_process
    • action_switch_screen
    • action_toggle_class
    • action_toggle_dark
    "},{"location":"guide/animation/","title":"Animation","text":"

    This 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 how the widget will change as time progresses:

    After 0sAfter 1sAfter 1.5sAfter 2s

    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/#run-inline","title":"Run inline","text":"

    Added in version 0.55.0

    You can also run apps in inline mode, which will cause the app to appear beneath the prompt (and won't go in to application mode). Inline apps are useful for tools that integrate closely with the typical workflow of a terminal.

    To run an app in inline mode set the inline parameter to True when you call App.run(). See Style Inline Apps for how to apply additional styles to inline apps.

    Note

    Inline mode is not currently supported on Windows.

    "},{"location":"guide/app/#ansi-colors","title":"ANSI colors","text":"

    Added in version 0.80.0

    Terminals support 16 theme-able ANSI colors, which you can personalize from your terminal settings. By default, Textual will replace these colors with its own color choices (see the FAQ for details).

    You can disable this behavior by setting ansi_color=True in the App constructor.

    We recommend the default behavior for full-screen apps, but you may want to preserve ANSI colors in inline apps.

    "},{"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+P 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'

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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\udd0eSearch\u00a0for\u00a0commands\u2026 Bell Ring\u00a0the\u00a0bell Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen Show\u00a0keys\u00a0and\u00a0help\u00a0panel Show\u00a0help\u00a0for\u00a0the\u00a0focused\u00a0widget\u00a0and\u00a0a\u00a0summary\u00a0of\u00a0available\u00a0keys \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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 Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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 Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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/#system-commands","title":"System commands","text":"

    Textual apps have a number of system commands enabled by default. These are declared in the App.get_system_commands method. You can implement this method in your App class to add more commands.

    To declare a command, define a get_system_commands method on your App. Textual will call this method with the screen that was active when the user summoned the command palette.

    You can add a command by yielding a SystemCommand object which contains title and help text to be shown in the command palette, and callback which is a callable to run when the user selects the command. Additionally, there is a discover boolean which when True (the default) shows the command even if the search import is empty. When set to False, the command will show only when there is input.

    Here's how we would add a command to ring the terminal bell (a super useful piece of functionality):

    command01.pyOutput command01.py
    from typing import Iterable\n\nfrom textual.app import App, SystemCommand\nfrom textual.screen import Screen\n\n\nclass BellCommandApp(App):\n    \"\"\"An app with a 'bell' command.\"\"\"\n\n    def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:\n        yield from super().get_system_commands(screen)  # (1)!\n        yield SystemCommand(\"Bell\", \"Ring the bell\", self.bell)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = BellCommandApp()\n    app.run()\n
    1. Adds the default commands from the base class.
    2. Adds a new command.

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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\udd0eSearch\u00a0for\u00a0commands\u2026 Bell Ring\u00a0the\u00a0bell Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen Show\u00a0keys\u00a0and\u00a0help\u00a0panel Show\u00a0help\u00a0for\u00a0the\u00a0focused\u00a0widget\u00a0and\u00a0a\u00a0summary\u00a0of\u00a0available\u00a0keys \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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 is a straightforward way of adding commands to your app. For more advanced integrations you can implement your own command providers.

    "},{"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.

    command02.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 four methods you can override in a command provider: startup, search, discover 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/#discover-method","title":"discover method","text":"

    The discover method is responsible for providing results (or discovery hits) that should be shown to the user when the command palette input is empty; this is to aid in command discoverability.

    Note

    Because discover hits are shown the moment the command palette is opened, these should ideally be quick to generate; commands that might take time to generate are best left for search -- use discover to help the user easily find the most important commands.

    discover is similar to search but with these differences:

    • discover accepts no parameters (instead of the search value)
    • discover yields instances of DiscoveryHit (instead of instances of Hit)

    Instances of DiscoveryHit contain information about how the hit should be displayed, an optional help string, and a callback which will be run if the user selects that command.

    "},{"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/command_palette/#changing-command-palette-key","title":"Changing command palette key","text":"

    You can change the key that opens the command palette by setting the class variable COMMAND_PALETTE_BINDING on your app.

    Prior to version 0.77.0, Textual used the binding ctrl+backslash to launch the command palette. Here's how you would restore the older key binding:

    class NewPaletteBindingApp(App):\n    COMMAND_PALETTE_BINDING = \"ctrl+backslash\"\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/#serve","title":"Serve","text":"

    The devtools can also serve your application in a browser. Effectively turning your terminal app in to a web application!

    The serve sub-command is similar to run. Here's how you can serve an app launched from a Python file:

    textual serve my_app.py\n

    You can also serve a Textual app launched via a command. Here's an example:

    textual serve \"textual keys\"\n

    The syntax for launching an app in a module is slightly different from run. You need to specify the full command, including python. Here's how you would run the Textual demo:

    textual serve \"python -m textual\"\n

    Textual's builtin web-server is quite powerful. You can serve multiple instances of your application at once!

    Tip

    Textual serve is also useful when developing your app. If you make changes to your code, simply refresh the browser to update.

    There are some additional switches for serving Textual apps. Run the following for a list:

    textual serve --help\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.79.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, LOGGING and WORKER. 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,\u00a0ansi=None)\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,\u00a0ansi=None)\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,\u00a0ansi=None)\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,\u00a0ansi=None)\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 its Changed message as follows:

    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.pyon_decorator.tcssOutput 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_decorator.tcss
    Screen {\n    align: center middle;\n    layout: horizontal;\n}\n\nButton {\n    margin: 2 4;\n}\n

    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.pyon_decorator.tcssOutput 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\"
    on_decorator.tcss
    Screen {\n    align: center middle;\n    layout: horizontal;\n}\n\nButton {\n    margin: 2 4;\n}\n

    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, pane=\"#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 Key(key='l',\u00a0character='l',\u00a0name='l'\u258eKey(key='r',\u00a0character='r',\u00a0name='r'\u258a Key(key='o',\u00a0character='o',\u00a0name='o'\u258eKey(key='l',\u00a0character='l',\u00a0name='l'\u2583\u2583\u258a Key(\u2586\u2586\u258eKey(key='d',\u00a0character='d',\u00a0name='d'\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u258ename='exclamation_mark',\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\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

    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/#focusable-widgets","title":"Focusable widgets","text":"

    Each widget has a boolean can_focus attribute which determines if it is capable of receiving focus. Note that can_focus=True does not mean the widget will always be focusable. For example, a disabled widget cannot receive focus even if can_focus is True.

    "},{"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. By default, Textual will focus the first focusable widget when the app starts.

    "},{"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\u00a0Add\u00a0Red\u00a0\u00a0g\u00a0Add\u00a0Green\u00a0\u00a0b\u00a0Add\u00a0Blue\u00a0\u258f^p\u00a0palette

    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').

    When you press a key, Textual will first check for a matching binding in the BINDINGS list of the currently focused widget. If no match is found, it will search upwards through the DOM all the way up to the App looking for a match.

    "},{"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.

    Both Enter and Leave bubble, so a widget may receive these events from a child widget. You can check the initial widget these events were sent to by comparing the node attribute against self in the message handler.

    "},{"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\" behavior 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!

    Tip

    See the Textual Query Sandbox project for an interactive way of experimenting with DOM queries.

    "},{"location":"guide/queries/#query-one","title":"Query one","text":"

    The query_one method is used to retrieve a single widget that matches a selector or a type.

    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 line of code:

    send_button = self.query_one(\"#send\")\n

    This will retrieve a widget with an ID of send, if there is exactly one. If there are no matching widgets, Textual will raise a NoMatches exception.

    You can also add a second parameter for the expected type, which will ensure that you get the type you are expecting.

    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).

    You can also specify a widget type in place of a selector, which will return a widget of that type. For instance, the following would return a Button instance (assuming there is a single Button).

    my_button = self.query_one(Button)\n
    "},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

    Apps and widgets also 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:

    • add_class Adds a CSS class (or classes) to matched widgets.
    • blur Blurs (removes focus) from matching widgets.
    • focus Focuses the first matching widgets.
    • refresh Refreshes matched widgets.
    • remove_class Removes a CSS class (or classes) from matched widgets.
    • remove Removes matched widgets from the DOM.
    • set_class Sets a CSS class (or classes) on matched widgets.
    • set Sets common attributes on a widget.
    • 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.
    "},{"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 behavior 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 10 \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\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\u250110%

    "},{"location":"guide/reactivity/#recompose","title":"Recompose","text":"

    An alternative to a refresh is recompose. If you set recompose=True on a reactive, then Textual will remove all the child widgets and call compose() again, when the reactive attribute changes. The process of removing and mounting new widgets occurs in a single update, so it will appear as though the content has simply updated.

    The following example uses recompose:

    refresh03.pyrefresh03.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\", recompose=True)  # (1)!\n\n    def compose(self) -> ComposeResult:  # (2)!\n        yield Label(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. Setting recompose=True will cause all child widgets to be removed and compose called again to add new widgets.
    2. This compose() method will be called when who is 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 \u258aPaul\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,\u00a0Paul!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    While the end-result is identical to refresh02.py, this code works quite differently. The main difference is that recomposing creates an entirely new set of child widgets rather than updating existing widgets. So when the who attribute changes, the Name widget will replace its Label with a new instance (containing updated content).

    Warning

    You should avoid storing a reference to child widgets when using recompose. Better to query for a child widget when you need them.

    It is important to note that any child widgets will have their state reset after a recompose. For simple content, that doesn't matter much. But widgets with an internal state (such as DataTable, Input, or TextArea) would not be particularly useful if recomposed.

    Recomposing is slightly less efficient than a simple refresh, and best avoided if you need to update rapidly or you have many child widgets. That said, it can often simplify your code. Let's look at a practical example. First a version without recompose:

    recompose01.pyOutput
    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Digits\n\n\nclass Clock(App):\n\n    CSS = \"\"\"\n    Screen {align: center middle}\n    Digits {width: auto}\n    \"\"\"\n\n    time: reactive[datetime] = reactive(datetime.now, init=False)\n\n    def compose(self) -> ComposeResult:\n        yield Digits(f\"{self.time:%X}\")\n\n    def watch_time(self) -> None:  # (1)!\n        self.query_one(Digits).update(f\"{self.time:%X}\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.set_interval(1, self.update_time)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = Clock()\n    app.run()\n
    1. Called when the time attribute changes.
    2. Update the time once a second.

    Clock \u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578 \u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2517\u2501\u2513 \u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u257a\u2501\u251b

    This displays a clock which updates once a second. The code is straightforward, but note how we format the time in two places: compose() and watch_time(). We can simplify this by recomposing rather than refreshing:

    recompose02.pyOutput
    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Digits\n\n\nclass Clock(App):\n\n    CSS = \"\"\"\n    Screen {align: center middle}\n    Digits {width: auto}\n    \"\"\"\n\n    time: reactive[datetime] = reactive(datetime.now, recompose=True)\n\n    def compose(self) -> ComposeResult:\n        yield Digits(f\"{self.time:%X}\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    app = Clock()\n    app.run()\n

    Clock \u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578 \u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2517\u2501\u2513 \u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u257a\u2501\u251b

    In this version, the app is recomposed when the time attribute changes, which replaces the Digits widget with a new instance and updated time. There's no need for the watch_time method, because the new Digits instance will already show the current time.

    "},{"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/reactivity/#setting-reactives-without-superpowers","title":"Setting reactives without superpowers","text":"

    You may find yourself in a situation where you want to set a reactive value, but you don't want to invoke watchers or the other super powers. This is fairly common in constructors which run prior to mounting; any watcher which queries the DOM may break if the widget has not yet been mounted.

    To work around this issue, you can call set_reactive as an alternative to setting the attribute. The set_reactive method accepts the reactive attribute (as a class variable) and the new value.

    Let's look at an example. The following app is intended to cycle through various greeting when you press Space, however it contains a bug.

    set_reactive01.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive, var\nfrom textual.widgets import Label\n\nGREETINGS = [\n    \"Bonjour\",\n    \"Hola\",\n    \"\u3053\u3093\u306b\u3061\u306f\",\n    \"\u4f60\u597d\",\n    \"\uc548\ub155\ud558\uc138\uc694\",\n    \"Hello\",\n]\n\n\nclass Greeter(Horizontal):\n    \"\"\"Display a greeting and a name.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Greeter {\n        width: auto;\n        height: 1;\n        & Label {\n            margin: 0 1;\n        }\n    }\n    \"\"\"\n    greeting: reactive[str] = reactive(\"\")\n    who: reactive[str] = reactive(\"\")\n\n    def __init__(self, greeting: str = \"Hello\", who: str = \"World!\") -> None:\n        super().__init__()\n        self.greeting = greeting  # (1)!\n        self.who = who\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.greeting, id=\"greeting\")\n        yield Label(self.who, id=\"name\")\n\n    def watch_greeting(self, greeting: str) -> None:\n        self.query_one(\"#greeting\", Label).update(greeting)  # (2)!\n\n    def watch_who(self, who: str) -> None:\n        self.query_one(\"#who\", Label).update(who)\n\n\nclass NameApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n    greeting_no: var[int] = var(0)\n    BINDINGS = [(\"space\", \"greeting\")]\n\n    def compose(self) -> ComposeResult:\n        yield Greeter(who=\"Textual\")\n\n    def action_greeting(self) -> None:\n        self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)\n        self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]\n\n\nif __name__ == \"__main__\":\n    app = NameApp()\n    app.run()\n
    1. Setting this reactive attribute invokes a watcher.
    2. The watcher attempts to update a label before it is mounted.

    If you run this app, you will find Textual raises a NoMatches error in watch_greeting. This is because the constructor has assigned the reactive before the widget has fully mounted.

    The following app contains a fix for this issue:

    set_reactive02.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive, var\nfrom textual.widgets import Label\n\nGREETINGS = [\n    \"Bonjour\",\n    \"Hola\",\n    \"\u3053\u3093\u306b\u3061\u306f\",\n    \"\u4f60\u597d\",\n    \"\uc548\ub155\ud558\uc138\uc694\",\n    \"Hello\",\n]\n\n\nclass Greeter(Horizontal):\n    \"\"\"Display a greeting and a name.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Greeter {\n        width: auto;\n        height: 1;\n        & Label {\n            margin: 0 1;\n        }\n    }\n    \"\"\"\n    greeting: reactive[str] = reactive(\"\")\n    who: reactive[str] = reactive(\"\")\n\n    def __init__(self, greeting: str = \"Hello\", who: str = \"World!\") -> None:\n        super().__init__()\n        self.set_reactive(Greeter.greeting, greeting)  # (1)!\n        self.set_reactive(Greeter.who, who)\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.greeting, id=\"greeting\")\n        yield Label(self.who, id=\"name\")\n\n    def watch_greeting(self, greeting: str) -> None:\n        self.query_one(\"#greeting\", Label).update(greeting)\n\n    def watch_who(self, who: str) -> None:\n        self.query_one(\"#who\", Label).update(who)\n\n\nclass NameApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n    greeting_no: var[int] = var(0)\n    BINDINGS = [(\"space\", \"greeting\")]\n\n    def compose(self) -> ComposeResult:\n        yield Greeter(who=\"Textual\")\n\n    def action_greeting(self) -> None:\n        self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)\n        self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]\n\n\nif __name__ == \"__main__\":\n    app = NameApp()\n    app.run()\n
    1. The attribute is set via set_reactive, which avoids calling the watcher.

    NameApp HelloTextual

    The line self.set_reactive(Greeter.greeting, greeting) sets the greeting attribute but doesn't immediately invoke the watcher.

    "},{"location":"guide/reactivity/#mutable-reactives","title":"Mutable reactives","text":"

    Textual can detect when you set a reactive to a new value, but it can't detect when you mutate a value. In practice, this means that Textual can detect changes to basic types (int, float, str, etc.), but not if you update a collection, such as a list or dict.

    You can still use collections and other mutable objects in reactives, but you will need to call mutate_reactive after making changes for the reactive superpowers to work.

    Here's an example, that uses a reactive list:

    set_reactive03.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Label\n\n\nclass MultiGreet(App):\n    names: reactive[list[str]] = reactive(list, recompose=True)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Give me a name\")\n        for name in self.names:\n            yield Label(f\"Hello, {name}\")\n\n    def on_input_submitted(self, event: Input.Changed) -> None:\n        self.names.append(event.value)\n        self.mutate_reactive(MultiGreet.names)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = MultiGreet()\n    app.run()\n
    1. Creates a reactive list of strings.
    2. Explicitly mutate the reactive list.

    MultiGreet \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 \u258aGive\u00a0me\u00a0a\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 Hello,\u00a0Will

    Note the call to mutate_reactive. Without it, the display would not update when a new name is appended to the list.

    "},{"location":"guide/reactivity/#data-binding","title":"Data binding","text":"

    Reactive attributes may be bound (connected) to attributes on child widgets, so that changes to the parent are automatically reflected in the children. This can simplify working with compound widgets where the value of an attribute might be used in multiple places.

    To bind reactive attributes, call data_bind on a widget. This method accepts reactives (as class attributes) in positional arguments or keyword arguments.

    Let's look at an app that could benefit from data binding. In the following code we have a WorldClock widget which displays the time in any given timezone.

    Note

    This example uses the pytz library for working with timezones. You can install pytz with pip install pytz.

    world_clock01.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\")\n        yield WorldClock(\"Europe/Paris\")\n        yield WorldClock(\"Asia/Tokyo\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def watch_time(self, time: datetime) -> None:\n        for world_clock in self.query(WorldClock):  # (1)!\n            world_clock.time = time\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    app = WorldClockApp()\n    app.run()\n
    1. Update the time reactive attribute of every WorldClock.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u00a0\u2513\u00a0\u257a\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u250f\u2501\u251b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u2517\u2501\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u00a0\u2513\u00a0\u250f\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u2501\u251b\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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

    We've added three world clocks for London, Paris, and Tokyo. The clocks are kept up-to-date by watching the app's time reactive, and updating the clocks in a loop.

    While this approach works fine, it does require we take care to update every WorldClock we mount. Let's see how data binding can simplify this.

    The following app calls data_bind on the world clock widgets to connect the app's time with the widget's time attribute:

    world_clock02.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\").data_bind(WorldClockApp.time)  # (1)!\n        yield WorldClock(\"Europe/Paris\").data_bind(WorldClockApp.time)\n        yield WorldClock(\"Asia/Tokyo\").data_bind(WorldClockApp.time)\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    WorldClockApp().run()\n
    1. Bind the time attribute, so that changes to time will also change the time attribute on the WorldClock widgets. The data_bind method also returns the widget, so we can yield its return value.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u00a0\u2513\u00a0\u257a\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u250f\u2501\u251b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u2517\u2501\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u00a0\u2513\u00a0\u250f\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u2501\u251b\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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

    Note how the addition of the data_bind methods negates the need for the watcher in world_clock01.py.

    Note

    Data binding works in a single direction. Setting time on the app updates the clocks. But setting time on the clocks will not update time on the app.

    In the previous example app, the call to data_bind(WorldClockApp.time) worked because both reactive attributes were named time. If you want to bind a reactive attribute which has a different name, you can use keyword arguments.

    In the following app we have changed the attribute name on WorldClock from time to clock_time. We can make the app continue to work by changing the data_bind call to data_bind(clock_time=WorldClockApp.time):

    world_clock03.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    clock_time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_clock_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\").data_bind(\n            clock_time=WorldClockApp.time  # (1)!\n        )\n        yield WorldClock(\"Europe/Paris\").data_bind(clock_time=WorldClockApp.time)\n        yield WorldClock(\"Asia/Tokyo\").data_bind(clock_time=WorldClockApp.time)\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    WorldClockApp().run()\n
    1. Uses keyword arguments to bind the time attribute of WorldClockApp to clock_time on WorldClock.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u00a0\u2513\u00a0\u00a0\u2513\u00a0\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u00a0\u2513\u00a0\u257a\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u250f\u2501\u251b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u2517\u2501\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u00a0\u2513\u00a0\u250f\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u258a \u258e\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0:\u00a0\u2503\u00a0\u2503\u00a0\u00a0\u2503\u00a0:\u00a0\u2517\u2501\u252b\u2523\u2501\u2513\u258a \u258e\u257a\u253b\u2578\u257a\u2501\u251b\u00a0\u00a0\u00a0\u2517\u2501\u251b\u00a0\u00a0\u2579\u00a0\u00a0\u00a0\u00a0\u00a0\u2579\u2517\u2501\u251b\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

    "},{"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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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) -> 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\u00a0Dashboard\u00a0\u00a0s\u00a0Settings\u00a0\u00a0h\u00a0Help\u00a0\u258f^p\u00a0palette

    ModesApp Settings\u00a0Screen \u00a0d\u00a0Dashboard\u00a0\u00a0s\u00a0Settings\u00a0\u00a0h\u00a0Help\u00a0\u258f^p\u00a0palette

    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/screens/#screen-events","title":"Screen events","text":"

    Textual will send a ScreenSuspend event to screens that have become inactive due to another screen being pushed, or switching via a mode.

    When a screen becomes active, Textual will send a ScreenResume event to the newly active screen.

    These events can be useful if you want to disable processing for a screen that is no longer visible, for example.

    "},{"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 is an async framework powered by Python's asyncio library. While Textual doesn't require a particular test framework, it must provide support for asyncio testing.

    You can use any test framework you are familiar with, but we will be using pytest along with the pytest-asyncio plugin in this chapter.

    By default, the pytest-asyncio plugin requires each async test to be decorated with @pytest.mark.asyncio. You can avoid having to add this marker to every async test by setting asyncio_mode = auto in your pytest configuration or by running pytest with the --asyncio-mode=auto option.

    "},{"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\u00a0Go\u00a0Red\u00a0\u00a0g\u00a0Go\u00a0Green\u00a0\u00a0b\u00a0Go\u00a0Blue\u00a0\u258f^p\u00a0palette

    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/#focus-keybindings","title":"Focus & keybindings","text":"

    Widgets can have a list of associated key bindings, which let them call actions in response to key presses.

    A widget is able to handle key presses if it or one of its descendants has focus.

    Widgets aren't focusable by default. To allow a widget to be focused, we need to set can_focus=True when defining a widget subclass. Here's an example of a simple focusable widget:

    counter01.pycounter.tcssOutput counter01.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Static\n\n\nclass Counter(Static, can_focus=True):  # (1)!\n    \"\"\"A counter that can be incremented and decremented by pressing keys.\"\"\"\n\n    count = reactive(0)\n\n    def render(self) -> RenderResult:\n        return f\"Count: {self.count}\"\n\n\nclass CounterApp(App[None]):\n    CSS_PATH = \"counter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield Counter()\n        yield Counter()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = CounterApp()\n    app.run()\n
    1. Allow the widget to receive input focus.
    counter.tcss
    Counter {\n    background: $panel-darken-1;\n    padding: 1 2;\n    color: $text-muted;\n\n    &:focus {  /* (1)! */\n        background: $primary;\n        color: $text;\n        text-style: bold;\n        outline-left: thick $accent;\n    }\n}\n
    1. These styles are applied only when the widget has focus.

    CounterApp \u2588 \u2588Count:\u00a00 \u2588 Count:\u00a00 Count:\u00a00 \u258f^p\u00a0palette

    The app above contains three Counter widgets, which we can focus by clicking or using Tab and Shift+Tab.

    Now that our counter is focusable, let's add some keybindings to it to allow us to change the count using the keyboard. To do this, we add a BINDINGS class variable to Counter, with bindings for Up and Down. These new bindings are linked to the change_count action, which updates the count reactive attribute.

    With our bindings in place, we can now change the count of the currently focused counter using Up and Down.

    counter02.pycounter.tcssOutput counter02.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Static\n\n\nclass Counter(Static, can_focus=True):\n    \"\"\"A counter that can be incremented and decremented by pressing keys.\"\"\"\n\n    BINDINGS = [\n        (\"up,k\", \"change_count(1)\", \"Increment\"),  # (1)!\n        (\"down,j\", \"change_count(-1)\", \"Decrement\"),\n    ]\n\n    count = reactive(0)\n\n    def render(self) -> RenderResult:\n        return f\"Count: {self.count}\"\n\n    def action_change_count(self, amount: int) -> None:  # (2)!\n        self.count += amount\n\n\nclass CounterApp(App[None]):\n    CSS_PATH = \"counter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield Counter()\n        yield Counter()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = CounterApp()\n    app.run()\n
    1. Associates presses of Up or K with the change_count action, passing 1 as the argument to increment the count. The final argument (\"Increment\") is a user-facing label displayed in the footer when this binding is active.
    2. Called when the binding is triggered. Take care to add the action_ prefix to the method name.
    counter.tcss
    Counter {\n    background: $panel-darken-1;\n    padding: 1 2;\n    color: $text-muted;\n\n    &:focus {  /* (1)! */\n        background: $primary;\n        color: $text;\n        text-style: bold;\n        outline-left: thick $accent;\n    }\n}\n
    1. These styles are applied only when the widget has focus.

    CounterApp Count:\u00a01 \u2588 \u2588Count:\u00a0-2 \u2588 Count:\u00a00 \u00a0\u2191\u00a0Increment\u00a0\u00a0\u2193\u00a0Decrement\u00a0\u258f^p\u00a0palette

    "},{"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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    "},{"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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    "},{"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.

    This is also true for anything that could take a significant time (more than a few milliseconds) to complete. For instance, reading from a subprocess or doing compute heavy work.

    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, RenderResult\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) -> RenderResult:\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":"how-to/style-inline-apps/","title":"Style Inline Apps","text":"

    Version 0.55.0 of Textual added support for running apps inline (below the prompt). Running an inline app is as simple as adding inline=True to run().

    Your apps will typically run inline without modification, but you may want to make some tweaks for inline mode, which you can do with a little CSS. This How-To will explain how.

    Let's look at an inline app. The following app displays the the current time (and keeps it up to date).

    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(inline=True)  #  (1)!\n
    1. The inline=True runs the app inline.

    With Textual's default settings, this clock will be displayed in 5 lines; 3 for the digits and 2 for a top and bottom border.

    You can change the height or the border with CSS and the :inline pseudo-selector, which only matches rules in inline mode. Let's update this app to remove the default border, and increase the height:

    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        &:inline {\n            border: none;\n            height: 50vh;\n            Digits {\n                color: $success;\n            }\n        }\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(inline=True)\n

    The highlighted CSS targets online inline mode. By setting the height rule on Screen we can define how many lines the app should consume when it runs. Setting border: none removes the default border when running in inline mode.

    We've also added a rule to change the color of the clock when running inline.

    "},{"location":"how-to/style-inline-apps/#summary","title":"Summary","text":"

    Most apps will not require modification to run inline, but if you want to tweak the height and border you can write CSS that targets inline mode with the :inline pseudo-selector.

    "},{"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/hatch/","title":"Hatch","text":"

    The hatch style fills a widget's background with a repeating character for a pleasing textured effect.

    "},{"location":"styles/hatch/#syntax","title":"Syntax","text":"
    \nhatch: (<hatch> | CHARACTER) <color> [<percentage>]\n

    The hatch type can be specified with a constant, or a string. For example, cross for cross hatch, or \"T\" for a custom character.

    The color can be any Textual color value.

    An optional percentage can be used to set the opacity.

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

    An app to show a few hatch effects.

    Outputhatch.pyhatch.tcss

    HatchApp \u250c\u2500\u00a0cross\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0horizontal\u00a0\u2500\u2510\u250c\u2500\u00a0custom\u00a0\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0right\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2514\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\u2518\u2514\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\u2518\u2514\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\nHATCHES = (\"cross\", \"horizontal\", \"custom\", \"left\", \"right\")\n\n\nclass HatchApp(App):\n    CSS_PATH = \"hatch.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            for hatch in HATCHES:\n                static = Static(classes=f\"hatch {hatch}\")\n                static.border_title = hatch\n                with Vertical():\n                    yield static\n\n\nif __name__ == \"__main__\":\n    app = HatchApp()\n    app.run()\n
    .hatch {\n    height: 1fr;\n    border: solid $secondary;\n\n    &.cross {\n        hatch: cross $success;\n    }\n    &.horizontal {\n        hatch: horizontal $success 80%;\n    }\n    &.custom {\n        hatch: \"T\" $success 60%;\n    }\n    &.left {\n        hatch: left $success 40%;\n    }\n    &.right {\n        hatch: right $success 20%;\n    }\n}\n
    "},{"location":"styles/hatch/#css","title":"CSS","text":"
    /* Red cross hatch */\nhatch: cross red;\n/* Right diagonals, 50% transparent green. */\nhatch: right green 50%;\n/* T custom character in 80% blue. **/\nhatch: \"T\" blue 80%;\n
    "},{"location":"styles/hatch/#python","title":"Python","text":"
    widget.styles.hatch = (\"cross\", \"red\")\nwidget.styles.hatch = (\"right\", \"rgba(0,255,0,128)\")\nwidget.styles.hatch = (\"T\", \"blue\")\n
    "},{"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.
    • margin 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 columns and 5 rows */\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 color 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 color 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 color 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-background-active to set the scrollbar background color when the scrollbar is being dragged.
    • scrollbar-background-hover to set the scrollbar background 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-background-hover to set the scrollbar background 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-background-active to set the scrollbar background 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-background-active to set the scrollbar background 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-background-hover to set the scrollbar background 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;.

    Bases: Widget

    A simple clickable button.

    Parameters:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None ButtonVariant

    The variant of the button.

    'default' str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/button/#textual.widgets.Button(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button(variant)","title":"variant","text":""},{"location":"widgets/button/#textual.widgets.Button(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button(tooltip)","title":"tooltip","text":""},{"location":"widgets/button/#textual.widgets.Button.active_effect_duration","title":"active_effect_duration instance-attribute","text":"
    active_effect_duration = 0.2\n

    Amount of time in seconds the button 'press' animation lasts.

    "},{"location":"widgets/button/#textual.widgets.Button.label","title":"label class-attribute instance-attribute","text":"
    label = label\n

    The text label that appears within the button.

    "},{"location":"widgets/button/#textual.widgets.Button.variant","title":"variant class-attribute instance-attribute","text":"
    variant = variant\n

    The variant name for the button.

    "},{"location":"widgets/button/#textual.widgets.Button.Pressed","title":"Pressed","text":"
    Pressed(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.Pressed.button","title":"button instance-attribute","text":"
    button = button\n

    The button that was pressed.

    "},{"location":"widgets/button/#textual.widgets.Button.Pressed.control","title":"control property","text":"
    control\n

    An alias for Pressed.button.

    This will be the same value as Pressed.button.

    "},{"location":"widgets/button/#textual.widgets.Button.action_press","title":"action_press","text":"
    action_press()\n

    Activate a press of the button.

    "},{"location":"widgets/button/#textual.widgets.Button.error","title":"error classmethod","text":"
    error(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'error' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.error(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.error(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.error(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.error(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.error(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.error(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.press","title":"press","text":"
    press()\n

    Animate the button and send the Pressed message.

    Can be used to simulate the button being pressed by a user.

    Returns:

    Type Description Self

    The button instance.

    "},{"location":"widgets/button/#textual.widgets.Button.success","title":"success classmethod","text":"
    success(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'success' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.success(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.success(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.success(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.success(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.success(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.success(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.validate_label","title":"validate_label","text":"
    validate_label(label)\n

    Parse markup for self.label

    "},{"location":"widgets/button/#textual.widgets.Button.warning","title":"warning classmethod","text":"
    warning(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'warning' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.warning(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(disabled)","title":"disabled","text":""},{"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.

    Bases: ToggleButton

    A check box widget that represents a boolean value.

    Parameters:

    Name Type Description Default TextType

    The label for the toggle.

    '' bool

    The initial value of the toggle.

    False bool

    Should the button come before the label, or after?

    True str | None

    The name of the toggle.

    None str | None

    The ID of the toggle in the DOM.

    None str | None

    The CSS classes of the toggle.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    RenderableType | None = None,

    None"},{"location":"widgets/checkbox/#textual.widgets.Checkbox(label)","title":"label","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(value)","title":"value","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(button_first)","title":"button_first","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(name)","title":"name","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(id)","title":"id","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(classes)","title":"classes","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(disabled)","title":"disabled","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(tooltip)","title":"tooltip","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed","title":"Changed","text":"
    Changed(toggle_button, value)\n

    Bases: Changed

    Posted when the value of the checkbox changes.

    This message can be handled using an on_checkbox_changed method.

    Parameters:

    Name Type Description Default ToggleButton

    The toggle button sending the message.

    required bool

    The value of the toggle button.

    required"},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed(toggle_button)","title":"toggle_button","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed(value)","title":"value","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed.checkbox","title":"checkbox property","text":"
    checkbox\n

    The checkbox that was changed.

    "},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed.control","title":"control property","text":"
    control\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 Lady\u00a0Jessica 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\u2586\u2586 c\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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 Lady\u00a0Jessica 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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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.

    Bases: Widget

    A collapsible container.

    Parameters:

    Name Type Description Default Widget

    Contents that will be collapsed/expanded.

    () str

    Title of the collapsed/expanded contents.

    'Toggle' bool

    Default status of the contents.

    True str

    Collapsed symbol before the title.

    '\u25b6' str

    Expanded symbol before the title.

    '\u25bc' str | None

    The name of the collapsible.

    None str | None

    The ID of the collapsible in the DOM.

    None str | None

    The CSS classes of the collapsible.

    None bool

    Whether the collapsible is disabled or not.

    False"},{"location":"widgets/collapsible/#textual.widgets.Collapsible(*children)","title":"*children","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(title)","title":"title","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(collapsed)","title":"collapsed","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(collapsed_symbol)","title":"collapsed_symbol","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(expanded_symbol)","title":"expanded_symbol","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(name)","title":"name","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(id)","title":"id","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(classes)","title":"classes","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(disabled)","title":"disabled","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Collapsed","title":"Collapsed","text":"
    Collapsed(collapsible)\n

    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.

    Parameters:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Collapsed(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Expanded","title":"Expanded","text":"
    Expanded(collapsible)\n

    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.

    Parameters:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Expanded(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled","title":"Toggled","text":"
    Toggled(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:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled.collapsible","title":"collapsible instance-attribute","text":"
    collapsible = collapsible\n

    The collapsible that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled.control","title":"control property","text":"
    control\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\u2502 \u2502\u2502 \u2502Three\u00a0Flavours\u00a0Cornetto\u2502 \u2502\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u00a0of\u00a0\u2502 \u2502British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u2502 \u2502Shaun\u00a0of\u00a0the\u00a0Dead\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Strawberry\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502Hot\u00a0Fuzz\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Classico\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502The\u00a0World's\u00a0End\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Mint\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02013-07-19\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2587\u2587\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.

    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:

    Name Type Description Default Widget

    The widgets to switch between.

    () str | None

    The name of the content switcher.

    None str | None

    The ID of the content switcher in the DOM.

    None str | None

    The CSS classes of the content switcher.

    None bool

    Whether the content switcher is disabled or not.

    False str | None

    The ID of the initial widget to show, None or empty string for the first tab.

    None Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(*children)","title":"*children","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(name)","title":"name","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(id)","title":"id","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(classes)","title":"classes","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(disabled)","title":"disabled","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(initial)","title":"initial","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
    current = reactive[Optional[str]](None, init=False)\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.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content","title":"add_content","text":"
    add_content(widget, *, id=None, set_current=False)\n

    Add new content to the ContentSwitcher.

    Parameters:

    Name Type Description Default Widget

    A Widget to add.

    required str | None

    ID for the widget, or None if the widget already has an ID.

    None bool

    Set the new widget as current (which will cause it to display).

    False

    Returns:

    Type Description AwaitComplete

    An awaitable to wait for the new content to be mounted.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(widget)","title":"widget","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(id)","title":"id","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(set_current)","title":"set_current","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current","title":"watch_current","text":"
    watch_current(old, new)\n

    React to the current visible child choice being changed.

    Parameters:

    Name Type Description Default str | None

    The old widget ID (or None if there was no widget).

    required str | None

    The new widget ID (or None if nothing should be shown).

    required"},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current(old)","title":"old","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current(new)","title":"new","text":""},{"location":"widgets/data_table/","title":"DataTable","text":"

    A widget to display text in a table. This includes the ability to update data, use a cursor to navigate data, respond to mouse clicks, delete rows or columns, and individually render each cell as a Rich Text renderable. DataTable provides an efficiently displayed and updated table capable for most applications.

    Applications may have custom rules for formatting, numbers, repopulating tables after searching or filtering, and responding to selections. The widget emits events to interface with custom logic.

    • 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 may need to convert that coordinate to a cell key first using the coordinate_to_cell_key method.

    "},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

    A cursor allows navigating within a table with the keyboard or mouse. There are four cursor types: \"cell\" (the default), \"row\", \"column\", and \"none\".

    Change the cursor type by assigning to the cursor_type reactive attribute. The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute.

    Using the keyboard, arrow keys, Page Up, Page Down, Home and End move the cursor highlight, emitting a CellHighlighted message, then enter selects the cell, emitting a CellSelected message. If the cursor_type is row, then RowHighlighted and RowSelected are emitted, similarly for ColumnHighlighted and ColumnSelected.

    When moving the mouse over the table, a MouseMove event is emitted, the cell hovered over is styled, and the hover_coordinate reactive attribute is updated. Clicking the mouse then emits the CellHighlighted and CellSelected events.

    A new table starts with no cell highlighted, i.e., row and column are zero. You can force the first item to highlight with move_cursor(row=1, column=1). All row and column indexes start at one.

    Column CursorRow CursorCell CursorNo 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

    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\", \"none\"])\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
    "},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

    Cells can be updated 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 \u00a03\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a09\u00a0\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a012\u00a0\u00a0 \u00a05\u00a0\u00a0\u00a010\u00a0\u00a0\u00a015\u00a0\u00a0\u2581\u2581 \u00a06\u00a0\u00a0\u00a012\u00a0\u00a0\u00a018\u00a0\u00a0 \u00a07\u00a0\u00a0\u00a014\u00a0\u00a0\u00a021\u00a0\u00a0 \u00a08\u00a0\u00a0\u00a016\u00a0\u00a0\u00a024\u00a0\u00a0 \u00a09\u00a0\u00a0\u00a018\u00a0\u00a0\u00a027\u00a0\u00a0 \u00a010\u00a0\u00a020\u00a0\u00a0\u00a030\u00a0\u00a0 \u00a011\u00a0\u00a022\u00a0\u00a0\u00a033\u00a0\u00a0 \u00a012\u00a0\u00a024\u00a0\u00a0\u00a036\u00a0\u00a0 \u00a013\u00a0\u00a026\u00a0\u00a0\u00a039\u00a0\u00a0 \u00a014\u00a0\u00a028\u00a0\u00a0\u00a042\u00a0\u00a0 \u00a015\u00a0\u00a030\u00a0\u00a0\u00a045\u00a0\u00a0 \u00a016\u00a0\u00a032\u00a0\u00a0\u00a048\u00a0\u00a0 \u00a017\u00a0\u00a034\u00a0\u00a0\u00a051\u00a0\u00a0 \u00a018\u00a0\u00a036\u00a0\u00a0\u00a054\u00a0\u00a0 \u00a019\u00a0\u00a038\u00a0\u00a0\u00a057\u00a0\u00a0 \u00a020\u00a0\u00a040\u00a0\u00a0\u00a060\u00a0\u00a0 \u00a021\u00a0\u00a042\u00a0\u00a0\u00a063\u00a0\u00a0 \u00a022\u00a0\u00a044\u00a0\u00a0\u00a066\u00a0\u00a0 \u00a023\u00a0\u00a046\u00a0\u00a0\u00a069\u00a0\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 rows can be sorted using the sort method.

    There are three methods of using sort:

    • By Column. Pass columns in as parameters to sort by the natural order of one or more columns. Specify a column using either a ColumnKey instance or the key you supplied to add_column. For example, sort(\"country\", \"region\") would sort by country, and, when the country values are equal, by region.
    • By Key function. Pass a function as the key parameter to sort, similar to the key function parameter of Python's sorted built-in. The function will be called once per row with a tuple of all row values.
    • By both Column and Key function. You can specify which columns to include as parameters to your key function. For example, sort(\"hours\", \"rate\", key=lambda h, r: h*r) passes two values to the key function for each row.

    The reverse argument reverses the order of your sort. Note that correct sorting may require your key function to undo your formatting.

    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\u00a0Sort\u00a0By\u00a0Average\u00a0Time\u00a0\u00a0n\u00a0Sort\u00a0By\u00a0Last\u00a0Name\u00a0\u00a0c\u00a0Sort\u00a0By\u00a0Country\u00a0\u00a0d\u00a0S\u258f^p\u00a0palette

    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/#labeled-rows","title":"Labeled 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.

    Labeled 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 Style with 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. pageup Move one page up. pagedown Move one page down. ctrl+home Move to the top. ctrl+end Move to the bottom. home Move to the home position (leftmost column). end Move to the end position (rightmost column)."},{"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) if zebra_stripes. datatable--odd-row Target odd rows (row indices start at 0) if zebra_stripes.

    Bases: ScrollView, Generic[CellType]

    A tabular widget that contains data.

    Parameters:

    Name Type Description Default bool

    Whether the table header should be visible or not.

    True bool

    Whether the row labels should be shown or not.

    True int

    The number of rows, counting from the top, that should be fixed and still visible when the user scrolls down.

    0 int

    The number of columns, counting from the left, that should be fixed and still visible when the user scrolls right.

    0 bool

    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.

    False int

    The height, in number of cells, of the data table header.

    1 bool

    Whether the cursor should be visible when navigating the data table or not.

    True Literal['renderable', '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.

    'css' Literal['renderable', 'css']

    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.

    'renderable' CursorType

    The type of cursor to be used when navigating the data table with the keyboard.

    'cell' int

    The number of cells added on each side of each column. Setting this value to zero will likely make your table very hard to read.

    1 str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/data_table/#textual.widgets.DataTable(show_header)","title":"show_header","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(show_row_labels)","title":"show_row_labels","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(fixed_rows)","title":"fixed_rows","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(fixed_columns)","title":"fixed_columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(zebra_stripes)","title":"zebra_stripes","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(header_height)","title":"header_height","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(show_cursor)","title":"show_cursor","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_foreground_priority)","title":"cursor_foreground_priority","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_background_priority)","title":"cursor_background_priority","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_type)","title":"cursor_type","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cell_padding)","title":"cell_padding","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(name)","title":"name","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(id)","title":"id","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(classes)","title":"classes","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(disabled)","title":"disabled","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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    Binding(\"ctrl+home\", \"scroll_top\", \"Top\", show=False),\n    Binding(\n        \"ctrl+end\", \"scroll_bottom\", \"Bottom\", show=False\n    ),\n    Binding(\"home\", \"scroll_home\", \"Home\", show=False),\n    Binding(\"end\", \"scroll_end\", \"End\", show=False),\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. pageup Move one page up. pagedown Move one page down. ctrl+home Move to the top. ctrl+end Move to the bottom. home Move to the home position (leftmost column). end Move to the end position (rightmost column)."},{"location":"widgets/data_table/#textual.widgets.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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) if zebra_stripes. datatable--odd-row Target odd rows (row indices start at 0) if zebra_stripes."},{"location":"widgets/data_table/#textual.widgets.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.DataTable.columns","title":"columns instance-attribute","text":"
    columns = {}\n

    Metadata about the columns of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets.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.DataTable.cursor_column","title":"cursor_column property","text":"
    cursor_column\n

    The index of the column that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.cursor_coordinate","title":"cursor_coordinate class-attribute instance-attribute","text":"
    cursor_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.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.DataTable.cursor_row","title":"cursor_row property","text":"
    cursor_row\n

    The index of the row that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.cursor_type","title":"cursor_type class-attribute instance-attribute","text":"
    cursor_type = cursor_type\n

    The type of cursor of the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.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.DataTable.hover_column","title":"hover_column property","text":"
    hover_column\n

    The index of the column that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.hover_coordinate","title":"hover_coordinate class-attribute instance-attribute","text":"
    hover_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.DataTable.hover_row","title":"hover_row property","text":"
    hover_row\n

    The index of the row that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ordered_columns","title":"ordered_columns property","text":"
    ordered_columns\n

    The list of Columns in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ordered_rows","title":"ordered_rows property","text":"
    ordered_rows\n

    The list of Rows in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.row_count","title":"row_count property","text":"
    row_count\n

    The number of rows currently present in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.rows","title":"rows instance-attribute","text":"
    rows = {}\n

    Metadata about the rows of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.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.DataTable.zebra_stripes","title":"zebra_stripes class-attribute instance-attribute","text":"
    zebra_stripes = zebra_stripes\n

    Apply alternating styles, datatable--even-row and datatable-odd-row, to create a zebra effect, e.g., alternating light and dark backgrounds.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted","title":"CellHighlighted","text":"
    CellHighlighted(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.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
    cell_key = cell_key\n

    The key for the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
    coordinate = coordinate\n

    The coordinate of the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
    value = value\n

    The value in the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected","title":"CellSelected","text":"
    CellSelected(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.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
    cell_key = cell_key\n

    The key for the selected cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
    coordinate = coordinate\n

    The coordinate of the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.value","title":"value instance-attribute","text":"
    value = value\n

    The value in the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted","title":"ColumnHighlighted","text":"
    ColumnHighlighted(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.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.DataTable.ColumnHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column = cursor_column\n

    The x-coordinate of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected","title":"ColumnSelected","text":"
    ColumnSelected(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.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.DataTable.ColumnSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column = cursor_column\n

    The x-coordinate of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected","title":"HeaderSelected","text":"
    HeaderSelected(data_table, column_key, column_index, label)\n

    Bases: Message

    Posted when a column header/label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.DataTable.HeaderSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted","title":"RowHighlighted","text":"
    RowHighlighted(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.DataTable.RowHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row = cursor_row\n

    The y-coordinate of the cursor that highlighted the row.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key of the row that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected","title":"RowLabelSelected","text":"
    RowLabelSelected(data_table, row_key, row_index, label)\n

    Bases: Message

    Posted when a row label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.DataTable.RowSelected","title":"RowSelected","text":"
    RowSelected(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.DataTable.RowSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row = cursor_row\n

    The y-coordinate of the cursor that made the selection.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key of the row that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the cursor one page down.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_left","title":"action_page_left","text":"
    action_page_left()\n

    Move the cursor one page left.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_right","title":"action_page_right","text":"
    action_page_right()\n

    Move the cursor one page right.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the cursor one page up.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_bottom","title":"action_scroll_bottom","text":"
    action_scroll_bottom()\n

    Move the cursor and scroll to the bottom.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_end","title":"action_scroll_end","text":"
    action_scroll_end()\n

    Move the cursor and scroll to the rightmost column.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_home","title":"action_scroll_home","text":"
    action_scroll_home()\n

    Move the cursor and scroll to the leftmost column.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_top","title":"action_scroll_top","text":"
    action_scroll_top()\n

    Move the cursor and scroll to the top.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column","title":"add_column","text":"
    add_column(label, *, width=None, key=None, default=None)\n

    Add a column to the table.

    Parameters:

    Name Type Description Default TextType

    A str or Text object containing the label (shown top of column).

    required int | None

    Width of the column in cells or None to fit content.

    None str | None

    A key which uniquely identifies this column. If None, it will be generated for you.

    None CellType | None

    The value to insert into pre-existing rows.

    None

    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.DataTable.add_column(label)","title":"label","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(width)","title":"width","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(default)","title":"default","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_columns","title":"add_columns","text":"
    add_columns(*labels)\n

    Add a number of columns.

    Parameters:

    Name Type Description Default 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.DataTable.add_columns(*labels)","title":"*labels","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row","title":"add_row","text":"
    add_row(*cells, height=1, key=None, label=None)\n

    Add a row at the bottom of the DataTable.

    Parameters:

    Name Type Description Default CellType

    Positional arguments should contain cell data.

    () int | None

    The height of a row (in lines). Use None to auto-detect the optimal height.

    1 str | None

    A key which uniquely identifies this row. If None, it will be generated for you and returned.

    None TextType | None

    The label for the row. Will be displayed to the left if supplied.

    None

    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.DataTable.add_row(*cells)","title":"*cells","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(height)","title":"height","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(label)","title":"label","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_rows","title":"add_rows","text":"
    add_rows(rows)\n

    Add a number of rows at the bottom of the DataTable.

    Parameters:

    Name Type Description Default Iterable[Iterable[CellType]]

    Iterable of rows. A row is an iterable of cells.

    required

    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.DataTable.add_rows(rows)","title":"rows","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.clear","title":"clear","text":"
    clear(columns=False)\n

    Clear the table.

    Parameters:

    Name Type Description Default bool

    Also clear the columns.

    False

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.clear(columns)","title":"columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key","text":"
    coordinate_to_cell_key(coordinate)\n

    Return the key for the cell currently occupying this coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to exam the current cell key of.

    required

    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.DataTable.coordinate_to_cell_key(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell","title":"get_cell","text":"
    get_cell(row_key, column_key)\n

    Given a row key and column key, return the value of the corresponding cell.

    Parameters:

    Name Type Description Default RowKey | str

    The row key of the cell.

    required ColumnKey | str

    The column key of the cell.

    required

    Returns:

    Type Description CellType

    The value of the cell identified by the row and column keys.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_at","title":"get_cell_at","text":"
    get_cell_at(coordinate)\n

    Get the value from the cell occupying the given coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to retrieve the value from.

    required

    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.DataTable.get_cell_at(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_coordinate","title":"get_cell_coordinate","text":"
    get_cell_coordinate(row_key, column_key)\n

    Given a row key and column key, return the corresponding cell coordinate.

    Parameters:

    Name Type Description Default RowKey | str

    The row key of the cell.

    required ColumnKey | str

    The column key of the cell.

    required

    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.DataTable.get_cell_coordinate(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_coordinate(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column","title":"get_column","text":"
    get_column(column_key)\n

    Get the values from the column identified by the given column key.

    Parameters:

    Name Type Description Default ColumnKey | str

    The key of the column.

    required

    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.DataTable.get_column(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column_at","title":"get_column_at","text":"
    get_column_at(column_index)\n

    Get the values from the column at a given index.

    Parameters:

    Name Type Description Default int

    The index of the column.

    required

    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.DataTable.get_column_at(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column_index","title":"get_column_index","text":"
    get_column_index(column_key)\n

    Return the current index for the column identified by column_key.

    Parameters:

    Name Type Description Default ColumnKey | str

    The column key to find the current index of.

    required

    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.DataTable.get_column_index(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row","title":"get_row","text":"
    get_row(row_key)\n

    Get the values from the row identified by the given row key.

    Parameters:

    Name Type Description Default RowKey | str

    The key of the row.

    required

    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.DataTable.get_row(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_at","title":"get_row_at","text":"
    get_row_at(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:

    Name Type Description Default int

    The index of the row.

    required

    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.DataTable.get_row_at(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_height","title":"get_row_height","text":"
    get_row_height(row_key)\n

    Given a row key, return the height of that row in terminal cells.

    Parameters:

    Name Type Description Default RowKey

    The key of the row.

    required

    Returns:

    Type Description int

    The height of the row, measured in terminal character cells.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_height(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_index","title":"get_row_index","text":"
    get_row_index(row_key)\n

    Return the current index for the row identified by row_key.

    Parameters:

    Name Type Description Default RowKey | str

    The row key to find the current index of.

    required

    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.DataTable.get_row_index(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_column_index","title":"is_valid_column_index","text":"
    is_valid_column_index(column_index)\n

    Return a boolean indicating whether the column_index is within table bounds.

    Parameters:

    Name Type Description Default int

    The column index to check.

    required

    Returns:

    Type Description bool

    True if the column index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_column_index(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_coordinate","title":"is_valid_coordinate","text":"
    is_valid_coordinate(coordinate)\n

    Return a boolean indicating whether the given coordinate is valid.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to validate.

    required

    Returns:

    Type Description bool

    True if the coordinate is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_coordinate(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_row_index","title":"is_valid_row_index","text":"
    is_valid_row_index(row_index)\n

    Return a boolean indicating whether the row_index is within table bounds.

    Parameters:

    Name Type Description Default int

    The row index to check.

    required

    Returns:

    Type Description bool

    True if the row index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_row_index(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor","title":"move_cursor","text":"
    move_cursor(\n    *, row=None, column=None, animate=False, scroll=True\n)\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:

    Name Type Description Default int | None

    The new row to move the cursor to.

    None int | None

    The new column to move the cursor to.

    None bool

    Whether to animate the change of coordinates.

    False bool

    Scroll the cursor into view after moving.

    True"},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(row)","title":"row","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(column)","title":"column","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(animate)","title":"animate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(scroll)","title":"scroll","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_column","title":"refresh_column","text":"
    refresh_column(column_index)\n

    Refresh the column at the given index.

    Parameters:

    Name Type Description Default int

    The index of the column to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_column(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_coordinate","title":"refresh_coordinate","text":"
    refresh_coordinate(coordinate)\n

    Refresh the cell at a coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_coordinate(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_row","title":"refresh_row","text":"
    refresh_row(row_index)\n

    Refresh the row at the given index.

    Parameters:

    Name Type Description Default int

    The index of the row to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_row(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_column","title":"remove_column","text":"
    remove_column(column_key)\n

    Remove a column (identified by a key) from the DataTable.

    Parameters:

    Name Type Description Default ColumnKey | str

    The key identifying the column to remove.

    required

    Raises:

    Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_column(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_row","title":"remove_row","text":"
    remove_row(row_key)\n

    Remove a row (identified by a key) from the DataTable.

    Parameters:

    Name Type Description Default RowKey | str

    The key identifying the row to remove.

    required

    Raises:

    Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_row(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort","title":"sort","text":"
    sort(*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:

    Name Type Description Default ColumnKey | str

    One or more columns to sort by the values in.

    () Callable[[Any], Any] | None

    A function (or other callable) that returns a key to use for sorting purposes.

    None bool

    If True, the sort order will be reversed.

    False

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(columns)","title":"columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(reverse)","title":"reverse","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell","title":"update_cell","text":"
    update_cell(\n    row_key, column_key, value, *, update_width=False\n)\n

    Update the cell identified by the specified row key and column key.

    Parameters:

    Name Type Description Default RowKey | str

    The key identifying the row.

    required ColumnKey | str

    The key identifying the column.

    required CellType

    The new value to put inside the cell.

    required bool

    Whether to resize the column width to accommodate for the new cell content.

    False

    Raises:

    Type Description CellDoesNotExist

    When the supplied row_key and column_key cannot be found in the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(value)","title":"value","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(update_width)","title":"update_width","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at","title":"update_cell_at","text":"
    update_cell_at(coordinate, value, *, update_width=False)\n

    Update the content inside the cell currently occupying the given coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to update the cell at.

    required CellType

    The new value to place inside the cell.

    required bool

    Whether to resize the column width to accommodate for the new cell content.

    False"},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(value)","title":"value","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(update_width)","title":"update_width","text":""},{"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","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","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\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\n

    The key of this cell's row.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column dataclass","text":"
    Column(\n    key, label, width=0, content_width=0, auto_width=False\n)\n

    Metadata for a column in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column.get_render_width","title":"get_render_width","text":"
    get_render_width(data_table)\n

    Width, in cells, required to render the column with padding included.

    Parameters:

    Name Type Description Default DataTable[Any]

    The data table where the column will be rendered.

    required

    Returns:

    Type Description int

    The width, in cells, required to render the column with padding included.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column.get_render_width(data_table)","title":"data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist","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","text":"
    ColumnKey(value=None)\n

    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","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 dataclass","text":"
    Row(key, height, label=None, auto_height=False)\n

    Metadata for a row in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist","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","text":"
    RowKey(value=None)\n

    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/data_table/#textual.widgets.data_table.StringKey","title":"StringKey","text":"
    StringKey(value=None)\n

    An object used as a key in a mapping.

    It can optionally wrap a string, and lookups into a map using the object behave the same as lookups using the string itself.

    "},{"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\u250f\u2501\u2513\u250f\u2501\u2513\u00a0\u00a0\u00a0\u00a0\u2513\u00a0\u250f\u2501\u2578 \u00a0\u2503\u00a0\u00a0\u2503\u00a0\u00a0:\u00a0\u2503\u00a0\u2503\u2523\u2501\u252b\u00a0:\u00a0\u00a0\u2503\u00a0\u2517\u2501\u2513 \u257a\u253b\u2578\u257a\u253b\u2578\u00a0\u00a0\u00a0\u2517\u2501\u251b\u2517\u2501\u251b\u00a0\u00a0\u00a0\u257a\u253b\u2578\u257a\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(inline=True)\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.

    Bases: Widget

    A widget to display numerical values using a 3x3 grid of unicode characters.

    Parameters:

    Name Type Description Default str

    Value to display in widget.

    '' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/digits/#textual.widgets.Digits(value)","title":"value","text":""},{"location":"widgets/digits/#textual.widgets.Digits(name)","title":"name","text":""},{"location":"widgets/digits/#textual.widgets.Digits(id)","title":"id","text":""},{"location":"widgets/digits/#textual.widgets.Digits(classes)","title":"classes","text":""},{"location":"widgets/digits/#textual.widgets.Digits(disabled)","title":"disabled","text":""},{"location":"widgets/digits/#textual.widgets.Digits.value","title":"value property","text":"
    value\n

    The current value displayed in the Digits.

    "},{"location":"widgets/digits/#textual.widgets.Digits.update","title":"update","text":"
    update(value)\n

    Update the Digits with a new value.

    Parameters:

    Name Type Description Default str

    New value to display.

    required

    Raises:

    Type Description ValueError

    If the value isn't a str.

    "},{"location":"widgets/digits/#textual.widgets.Digits.update(value)","title":"value","text":""},{"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

    Bases: Tree[DirEntry]

    A Tree widget that presents files and directories.

    Parameters:

    Name Type Description Default str | Path

    Path to directory.

    required str | None

    The name of the widget, or None for no name.

    None str | None

    The ID of the widget in the DOM, or None for no ID.

    None str | None

    A space-separated list of classes, or None for no classes.

    None bool

    Whether the directory tree is disabled or not.

    False"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(name)","title":"name","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(id)","title":"id","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(classes)","title":"classes","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(disabled)","title":"disabled","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.DirectoryTree.ICON_FILE","title":"ICON_FILE class-attribute instance-attribute","text":"
    ICON_FILE = '\ud83d\udcc4 '\n

    Unicode 'icon' to represent a file.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.PATH","title":"PATH class-attribute instance-attribute","text":"
    PATH = Path\n

    Callable that returns a fresh path object.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.path","title":"path class-attribute instance-attribute","text":"
    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.DirectoryTree.DirectorySelected","title":"DirectorySelected","text":"
    DirectorySelected(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:

    Name Type Description Default TreeNode[DirEntry]

    The tree node for the directory that was selected.

    required Path

    The path of the directory that was selected.

    required"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.control","title":"control property","text":"
    control\n

    The Tree that had a directory selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
    node = node\n

    The tree node of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
    path = path\n

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected","title":"FileSelected","text":"
    FileSelected(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:

    Name Type Description Default TreeNode[DirEntry]

    The tree node for the file that was selected.

    required Path

    The path of the file that was selected.

    required"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.control","title":"control property","text":"
    control\n

    The Tree that had a file selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
    node = node\n

    The tree node of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
    path = path\n

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.clear_node","title":"clear_node","text":"
    clear_node(node)\n

    Clear all nodes under the given node.

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.filter_paths","title":"filter_paths","text":"
    filter_paths(paths)\n

    Filter the paths before adding them to the tree.

    Parameters:

    Name Type Description Default Iterable[Path]

    The paths to be filtered.

    required

    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.DirectoryTree.filter_paths(paths)","title":"paths","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.process_label","title":"process_label","text":"
    process_label(label)\n

    Process a str or Text into a label. May be overridden in a subclass to modify how labels are rendered.

    Parameters:

    Name Type Description Default TextType

    Label.

    required

    Returns:

    Type Description Text

    A Rich Text object.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.process_label(label)","title":"label","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload","title":"reload","text":"
    reload()\n

    Reload the DirectoryTree contents.

    Returns:

    Type Description AwaitComplete

    An optionally awaitable that ensures the tree has finished reloading.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload_node","title":"reload_node","text":"
    reload_node(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:

    Name Type Description Default TreeNode[DirEntry]

    The root of the subtree to reload.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable that ensures the subtree has finished reloading.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload_node(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label","title":"render_label","text":"
    render_label(node, base_style, style)\n

    Render a label for the given node.

    Parameters:

    Name Type Description Default TreeNode[DirEntry]

    A tree node.

    required Style

    The base style of the widget.

    required Style

    The additional style for the label.

    required

    Returns:

    Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(base_style)","title":"base_style","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(style)","title":"style","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node","title":"reset_node","text":"
    reset_node(node, label, data=None)\n

    Clear the subtree and reset the given node.

    Parameters:

    Name Type Description Default TreeNode[DirEntry]

    The node to reset.

    required TextType

    The label for the node.

    required DirEntry | None

    Optional data for the node.

    None

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(label)","title":"label","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(data)","title":"data","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.validate_path","title":"validate_path","text":"
    validate_path(path)\n

    Ensure that the path is of the Path type.

    Parameters:

    Name Type Description Default str | Path

    The path to validate.

    required

    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.DirectoryTree.validate_path(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.watch_path","title":"watch_path async","text":"
    watch_path()\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":"

    Added in version 0.63.0

    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\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0del\u00a0Delete\u00a0the\u00a0thing\u00a0\u258f^p\u00a0palette

    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 compact bool False Display a more compact footer. show_command_palette bool True Display the key to invoke the command palette (show on the right hand side of 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":"

    This widget has no component classes.

    "},{"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.

    Bases: ScrollableContainer

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False bool

    Show key binding to command palette, on the right of the footer.

    True"},{"location":"widgets/footer/#textual.widgets.Footer(*children)","title":"*children","text":""},{"location":"widgets/footer/#textual.widgets.Footer(name)","title":"name","text":""},{"location":"widgets/footer/#textual.widgets.Footer(id)","title":"id","text":""},{"location":"widgets/footer/#textual.widgets.Footer(classes)","title":"classes","text":""},{"location":"widgets/footer/#textual.widgets.Footer(disabled)","title":"disabled","text":""},{"location":"widgets/footer/#textual.widgets.Footer(show_command_palette)","title":"show_command_palette","text":""},{"location":"widgets/footer/#textual.widgets.Footer.compact","title":"compact class-attribute instance-attribute","text":"
    compact = reactive(False)\n

    Display in compact style.

    "},{"location":"widgets/footer/#textual.widgets.Footer.show_command_palette","title":"show_command_palette class-attribute instance-attribute","text":"
    show_command_palette = reactive(True)\n

    Show the key to invoke the command palette.

    "},{"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.

    Bases: Widget

    A header widget with icon and clock.

    Parameters:

    Name Type Description Default bool

    True if the clock should be shown on the right of the header.

    False str | None

    The name of the header widget.

    None str | None

    The ID of the header widget in the DOM.

    None str | None

    The CSS classes of the header widget.

    None str | None

    Single character to use as an icon, or None for default.

    None str | None

    Time format (used by strftime) for clock, or None for default.

    None"},{"location":"widgets/header/#textual.widgets.Header(show_clock)","title":"show_clock","text":""},{"location":"widgets/header/#textual.widgets.Header(name)","title":"name","text":""},{"location":"widgets/header/#textual.widgets.Header(id)","title":"id","text":""},{"location":"widgets/header/#textual.widgets.Header(classes)","title":"classes","text":""},{"location":"widgets/header/#textual.widgets.Header(icon)","title":"icon","text":""},{"location":"widgets/header/#textual.widgets.Header(time_format)","title":"time_format","text":""},{"location":"widgets/header/#textual.widgets.Header.icon","title":"icon class-attribute instance-attribute","text":"
    icon = Reactive('\u2b58')\n

    A character for the icon at the top left.

    "},{"location":"widgets/header/#textual.widgets.Header.screen_sub_title","title":"screen_sub_title property","text":"
    screen_sub_title\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.screen_title","title":"screen_title property","text":"
    screen_title\n

    The title that this header will display.

    This depends on Screen.title and App.title.

    "},{"location":"widgets/header/#textual.widgets.Header.tall","title":"tall class-attribute instance-attribute","text":"
    tall = Reactive(False)\n

    Set to True for a taller header or False for a single line header.

    "},{"location":"widgets/header/#textual.widgets.Header.time_format","title":"time_format class-attribute instance-attribute","text":"
    time_format = Reactive('%X')\n

    Time format of the clock.

    "},{"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.

    Bases: Widget

    A text input widget.

    Parameters:

    Name Type Description Default str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Highlighter | None

    An optional highlighter for the input.

    None bool

    Flag to say if the field should obfuscate its content.

    False str | None

    A regex to restrict character inputs.

    None InputType

    The type of the input.

    'text' int

    The maximum length of the input, or 0 for no maximum length.

    0 Suggester | None

    Suggester associated with this input instance.

    None Validator | Iterable[Validator] | None

    An iterable of validators that the Input value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/input/#textual.widgets.Input(value)","title":"value","text":""},{"location":"widgets/input/#textual.widgets.Input(placeholder)","title":"placeholder","text":""},{"location":"widgets/input/#textual.widgets.Input(highlighter)","title":"highlighter","text":""},{"location":"widgets/input/#textual.widgets.Input(password)","title":"password","text":""},{"location":"widgets/input/#textual.widgets.Input(restrict)","title":"restrict","text":""},{"location":"widgets/input/#textual.widgets.Input(type)","title":"type","text":""},{"location":"widgets/input/#textual.widgets.Input(max_length)","title":"max_length","text":""},{"location":"widgets/input/#textual.widgets.Input(suggester)","title":"suggester","text":""},{"location":"widgets/input/#textual.widgets.Input(validators)","title":"validators","text":""},{"location":"widgets/input/#textual.widgets.Input(validate_on)","title":"validate_on","text":""},{"location":"widgets/input/#textual.widgets.Input(valid_empty)","title":"valid_empty","text":""},{"location":"widgets/input/#textual.widgets.Input(name)","title":"name","text":""},{"location":"widgets/input/#textual.widgets.Input(id)","title":"id","text":""},{"location":"widgets/input/#textual.widgets.Input(classes)","title":"classes","text":""},{"location":"widgets/input/#textual.widgets.Input(disabled)","title":"disabled","text":""},{"location":"widgets/input/#textual.widgets.Input(tooltip)","title":"tooltip","text":""},{"location":"widgets/input/#textual.widgets.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"left\",\n        \"cursor_left\",\n        \"Move cursor left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_left_word\",\n        \"Move cursor left a word\",\n        show=False,\n    ),\n    Binding(\n        \"right\",\n        \"cursor_right\",\n        \"Move cursor right\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_right_word\",\n        \"Move cursor right a word\",\n        show=False,\n    ),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"Delete character left\",\n        show=False,\n    ),\n    Binding(\n        \"home,ctrl+a\", \"home\", \"Go to start\", show=False\n    ),\n    Binding(\"end,ctrl+e\", \"end\", \"Go to end\", show=False),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"Delete character 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.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset\n

    The offset of the cursor of this input in screen-space. (x, y)/(column, row)

    "},{"location":"widgets/input/#textual.widgets.Input.cursor_width","title":"cursor_width property","text":"
    cursor_width\n

    The width of the input (with extra space for cursor at the end).

    "},{"location":"widgets/input/#textual.widgets.Input.is_valid","title":"is_valid property","text":"
    is_valid\n

    Check if the value has passed validation.

    "},{"location":"widgets/input/#textual.widgets.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.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.suggester","title":"suggester instance-attribute","text":"
    suggester = suggester\n

    The suggester used to provide completions as the user types.

    "},{"location":"widgets/input/#textual.widgets.Input.type","title":"type class-attribute instance-attribute","text":"
    type = type\n

    The type of the input.

    "},{"location":"widgets/input/#textual.widgets.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.validate_on","title":"validate_on instance-attribute","text":"
    validate_on = (\n    _POSSIBLE_VALIDATE_ON_VALUES & set(validate_on)\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.Changed","title":"Changed dataclass","text":"
    Changed(input, value, validation_result=None)\n

    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.Changed.control","title":"control property","text":"
    control\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets.Input.Changed.input","title":"input instance-attribute","text":"
    input\n

    The Input widget that was changed.

    "},{"location":"widgets/input/#textual.widgets.Input.Changed.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result = 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.Changed.value","title":"value instance-attribute","text":"
    value\n

    The value that the input was changed to.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted","title":"Submitted dataclass","text":"
    Submitted(input, value, validation_result=None)\n

    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.Submitted.control","title":"control property","text":"
    control\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted.input","title":"input instance-attribute","text":"
    input\n

    The Input widget that is being submitted.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result = 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.Submitted.value","title":"value instance-attribute","text":"
    value\n

    The value of the Input being submitted.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left()\n

    Move the cursor one position to the left.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_left_word","title":"action_cursor_left_word","text":"
    action_cursor_left_word()\n

    Move the cursor left to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_right","title":"action_cursor_right","text":"
    action_cursor_right()\n

    Accept an auto-completion or move the cursor one position to the right.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_right_word","title":"action_cursor_right_word","text":"
    action_cursor_right_word()\n

    Move the cursor right to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left","title":"action_delete_left","text":"
    action_delete_left()\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left_all","title":"action_delete_left_all","text":"
    action_delete_left_all()\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left_word","title":"action_delete_left_word","text":"
    action_delete_left_word()\n

    Delete leftward of the cursor position to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right","title":"action_delete_right","text":"
    action_delete_right()\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right_all","title":"action_delete_right_all","text":"
    action_delete_right_all()\n

    Delete the current character and all characters to the right of the cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right_word","title":"action_delete_right_word","text":"
    action_delete_right_word()\n

    Delete the current character and all rightward to the start of the next word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_end","title":"action_end","text":"
    action_end()\n

    Move the cursor to the end of the input.

    "},{"location":"widgets/input/#textual.widgets.Input.action_home","title":"action_home","text":"
    action_home()\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/input/#textual.widgets.Input.action_submit","title":"action_submit async","text":"
    action_submit()\n

    Handle a submit action.

    Normally triggered by the user pressing Enter. This may also run any validators.

    "},{"location":"widgets/input/#textual.widgets.Input.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character)\n

    Check if the widget may consume the given key.

    As an input we are expecting to capture printable keys.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    required

    Returns:

    Type Description bool

    True if the widget may capture the key in it's Key message, or False if it won't.

    "},{"location":"widgets/input/#textual.widgets.Input.check_consume_key(key)","title":"key","text":""},{"location":"widgets/input/#textual.widgets.Input.check_consume_key(character)","title":"character","text":""},{"location":"widgets/input/#textual.widgets.Input.clear","title":"clear","text":"
    clear()\n

    Clear the input.

    "},{"location":"widgets/input/#textual.widgets.Input.insert_text_at_cursor","title":"insert_text_at_cursor","text":"
    insert_text_at_cursor(text)\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters:

    Name Type Description Default str

    New text to insert.

    required"},{"location":"widgets/input/#textual.widgets.Input.insert_text_at_cursor(text)","title":"text","text":""},{"location":"widgets/input/#textual.widgets.Input.restricted","title":"restricted","text":"
    restricted()\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.validate","title":"validate","text":"
    validate(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.

    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 \u258f^p\u00a0palette

    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.

    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.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/list_item/#textual.widgets.ListItem(*children)","title":"*children","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(name)","title":"name","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(id)","title":"id","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(classes)","title":"classes","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(disabled)","title":"disabled","text":""},{"location":"widgets/list_item/#textual.widgets.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 \u258f^p\u00a0palette

    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.

    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:

    Name Type Description Default ListItem

    The ListItems to display in the list.

    () int | None

    The index that should be highlighted when the list is first mounted.

    0 str | None

    The name of the widget.

    None str | None

    The unique ID of the widget used in CSS/query selection.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the ListView is disabled or not.

    False"},{"location":"widgets/list_view/#textual.widgets.ListView(*children)","title":"*children","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(initial_index)","title":"initial_index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(name)","title":"name","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(id)","title":"id","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(classes)","title":"classes","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(disabled)","title":"disabled","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.ListView.highlighted_child","title":"highlighted_child property","text":"
    highlighted_child\n

    The currently highlighted ListItem, or None if nothing is highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.index","title":"index class-attribute instance-attribute","text":"
    index = reactive[Optional[int]](\n    0, always_update=True, init=False\n)\n

    The index of the currently highlighted item.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Highlighted","title":"Highlighted","text":"
    Highlighted(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.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.ListView.Highlighted.control","title":"control property","text":"
    control\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.ListView.Highlighted.item","title":"item instance-attribute","text":"
    item = item\n

    The highlighted item, if there is one highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
    list_view = list_view\n

    The view that contains the item highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Selected","title":"Selected","text":"
    Selected(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.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.ListView.Selected.control","title":"control property","text":"
    control\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.ListView.Selected.item","title":"item instance-attribute","text":"
    item = item\n

    The selected item.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
    list_view = list_view\n

    The view that contains the item selected.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Highlight the next item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Highlight the previous item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_select_cursor","title":"action_select_cursor","text":"
    action_select_cursor()\n

    Select the current item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.append","title":"append","text":"
    append(item)\n

    Append a new ListItem to the end of the ListView.

    Parameters:

    Name Type Description Default ListItem

    The ListItem to append.

    required

    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.ListView.append(item)","title":"item","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.clear","title":"clear","text":"
    clear()\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.ListView.extend","title":"extend","text":"
    extend(items)\n

    Append multiple new ListItems to the end of the ListView.

    Parameters:

    Name Type Description Default Iterable[ListItem]

    The ListItems to append.

    required

    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.ListView.extend(items)","title":"items","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.insert","title":"insert","text":"
    insert(index, items)\n

    Insert new ListItem(s) to specified index.

    Parameters:

    Name Type Description Default int

    index to insert new ListItem.

    required Iterable[ListItem]

    The ListItems to insert.

    required

    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.ListView.insert(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.insert(items)","title":"items","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.pop","title":"pop","text":"
    pop(index=None)\n

    Remove last ListItem from ListView or Remove ListItem from ListView by index

    Parameters:

    Name Type Description Default Optional[int]

    index of ListItem to remove from ListView

    None

    Returns:

    Type Description AwaitRemove

    An awaitable that yields control to the event loop until the DOM has been updated to reflect item being removed.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.pop(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.remove_items","title":"remove_items","text":"
    remove_items(indices)\n

    Remove ListItems from ListView by indices

    Parameters:

    Name Type Description Default Iterable[int]

    index(s) of ListItems to remove from ListView

    required

    Returns:

    Type Description AwaitRemove

    An awaitable object that waits for the direct children to be removed.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.remove_items(indices)","title":"indices","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.validate_index","title":"validate_index","text":"
    validate_index(index)\n

    Clamp the index to the valid range, or set to None if there's nothing to highlight.

    Parameters:

    Name Type Description Default int | None

    The index to clamp.

    required

    Returns:

    Type Description int | None

    The clamped index.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.validate_index(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.watch_index","title":"watch_index","text":"
    watch_index(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

    Tip

    Widgets have a loading reactive which you can use to temporarily replace your widget with a LoadingIndicator. See the Loading Indicator section in the Widgets guide for details.

    "},{"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.

    Bases: Widget

    Display an animated loading indicator.

    Parameters:

    Name Type Description Default str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(name)","title":"name","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(id)","title":"id","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(classes)","title":"classes","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(disabled)","title":"disabled","text":""},{"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.

    Bases: ScrollView

    A widget to log text.

    Parameters:

    Name Type Description Default bool

    Enable highlighting.

    False int | None

    Maximum number of lines to display.

    None bool

    Scroll to end on new lines.

    True str | None

    The name of the text log.

    None str | None

    The ID of the text log in the DOM.

    None str | None

    The CSS classes of the text log.

    None bool

    Whether the text log is disabled or not.

    False"},{"location":"widgets/log/#textual.widgets.Log(highlight)","title":"highlight","text":""},{"location":"widgets/log/#textual.widgets.Log(max_lines)","title":"max_lines","text":""},{"location":"widgets/log/#textual.widgets.Log(auto_scroll)","title":"auto_scroll","text":""},{"location":"widgets/log/#textual.widgets.Log(name)","title":"name","text":""},{"location":"widgets/log/#textual.widgets.Log(id)","title":"id","text":""},{"location":"widgets/log/#textual.widgets.Log(classes)","title":"classes","text":""},{"location":"widgets/log/#textual.widgets.Log(disabled)","title":"disabled","text":""},{"location":"widgets/log/#textual.widgets.Log.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll = auto_scroll\n

    Automatically scroll to new lines.

    "},{"location":"widgets/log/#textual.widgets.Log.highlight","title":"highlight instance-attribute","text":"
    highlight = highlight\n

    Enable highlighting.

    "},{"location":"widgets/log/#textual.widgets.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.line_count","title":"line_count property","text":"
    line_count\n

    Number of lines of content.

    "},{"location":"widgets/log/#textual.widgets.Log.lines","title":"lines property","text":"
    lines\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.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines = max_lines\n

    Maximum number of lines to show

    "},{"location":"widgets/log/#textual.widgets.Log.clear","title":"clear","text":"
    clear()\n

    Clear the Log.

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\n

    Called by Textual when styles update.

    "},{"location":"widgets/log/#textual.widgets.Log.refresh_lines","title":"refresh_lines","text":"
    refresh_lines(y_start, line_count=1)\n

    Refresh one or more lines.

    Parameters:

    Name Type Description Default int

    First line to refresh.

    required int

    Total number of lines to refresh.

    1"},{"location":"widgets/log/#textual.widgets.Log.refresh_lines(y_start)","title":"y_start","text":""},{"location":"widgets/log/#textual.widgets.Log.refresh_lines(line_count)","title":"line_count","text":""},{"location":"widgets/log/#textual.widgets.Log.write","title":"write","text":"
    write(data, scroll_end=None)\n

    Write to the log.

    Parameters:

    Name Type Description Default str

    Data to write.

    required bool | None

    Scroll to the end after writing, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write(data)","title":"data","text":""},{"location":"widgets/log/#textual.widgets.Log.write(scroll_end)","title":"scroll_end","text":""},{"location":"widgets/log/#textual.widgets.Log.write_line","title":"write_line","text":"
    write_line(line)\n

    Write content on a new line.

    Parameters:

    Name Type Description Default str

    String to write to the log.

    required

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write_line(line)","title":"line","text":""},{"location":"widgets/log/#textual.widgets.Log.write_lines","title":"write_lines","text":"
    write_lines(lines, scroll_end=None)\n

    Write an iterable of lines.

    Parameters:

    Name Type Description Default Iterable[str]

    An iterable of strings to write.

    required bool | None

    Scroll to the end after writing, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write_lines(lines)","title":"lines","text":""},{"location":"widgets/log/#textual.widgets.Log.write_lines(scroll_end)","title":"scroll_end","text":""},{"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 Markdown\u00a0Document This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. Features 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

    Bases: Widget

    Parameters:

    Name Type Description Default str | None

    String containing Markdown or None to leave blank for now.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown/#textual.widgets.Markdown(markdown)","title":"markdown","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(name)","title":"name","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(id)","title":"id","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(classes)","title":"classes","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(parser_factory)","title":"parser_factory","text":""},{"location":"widgets/markdown/#textual.widgets.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.code_dark_theme","title":"code_dark_theme class-attribute instance-attribute","text":"
    code_dark_theme = reactive('material')\n

    The theme to use for code blocks when in dark mode.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.code_light_theme","title":"code_light_theme class-attribute instance-attribute","text":"
    code_light_theme = reactive('material-light')\n

    The theme to use for code blocks when in light mode.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked","title":"LinkClicked","text":"
    LinkClicked(markdown, href)\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked.control","title":"control property","text":"
    control\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.LinkClicked.href","title":"href instance-attribute","text":"
    href = unquote(href)\n

    The link that was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected","text":"
    TableOfContentsSelected(markdown, block_id)\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control\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.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated","text":"
    TableOfContentsUpdated(markdown, table_of_contents)\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control\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.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.goto_anchor","title":"goto_anchor","text":"
    goto_anchor(anchor)\n

    Try and find the given anchor in the current document.

    Parameters:

    Name Type Description Default str

    The anchor to try and find.

    required 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.goto_anchor(anchor)","title":"anchor","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.load","title":"load async","text":"
    load(path)\n

    Load a new Markdown document.

    Parameters:

    Name Type Description Default Path

    Path to the document.

    required

    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.load(path)","title":"path","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    sanitize_location(location)\n

    Given a location, break out the path and any anchor.

    Parameters:

    Name Type Description Default str

    The location to sanitize.

    required

    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.sanitize_location(location)","title":"location","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.unhandled_token","title":"unhandled_token","text":"
    unhandled_token(token)\n

    Process an unhandled token.

    Parameters:

    Name Type Description Default Token

    The MarkdownIt token to handle.

    required

    Returns:

    Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.unhandled_token(token)","title":"token","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.update","title":"update","text":"
    update(markdown)\n

    Update the document with new Markdown.

    Parameters:

    Name Type Description Default str

    A string containing Markdown.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.update(markdown)","title":"markdown","text":""},{"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 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258aMarkdown\u00a0Viewer \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a \u258aFeatures \u258a \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 \u258aTables \u258a \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a \u258aName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Type\u00a0Default\u00a0Description\u00a0\u00a0\u00a0\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u00a0 \u258ashow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258azebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u00a0\u00a0\u00a0\u00a0 \u258aheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258ashow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u258a \u258a \u258aCode\u00a0Blocks \u258a\u2585\u2585 \u258aCode\u00a0blocks\u00a0are\u00a0syntax\u00a0highlighted,\u00a0with\u00a0guidelines. \u258a \u258a \u258aclassListViewExample(App): \u258a\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u258a\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldListView(

    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

    Bases: VerticalScroll

    A Markdown viewer widget.

    Parameters:

    Name Type Description Default str | None

    String containing Markdown, or None to leave blank.

    None bool

    Show a table of contents in a sidebar.

    True str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(show_table_of_contents)","title":"show_table_of_contents","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(parser_factory)","title":"parser_factory","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.document","title":"document property","text":"
    document\n

    The Markdown document widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
    table_of_contents\n

    The table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.NavigatorUpdated","title":"NavigatorUpdated","text":"
    NavigatorUpdated()\n

    Bases: Message

    Navigator has been changed (clicked link etc).

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.back","title":"back async","text":"
    back()\n

    Go back one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.forward","title":"forward async","text":"
    forward()\n

    Go forward one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.go","title":"go async","text":"
    go(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 = 'list[tuple[int, str, str | None]]'\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","text":"
    Markdown(\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n)\n

    Bases: Widget

    Parameters:

    Name Type Description Default str | None

    String containing Markdown or None to leave blank for now.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(parser_factory)","title":"parser_factory","text":""},{"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.code_dark_theme","title":"code_dark_theme class-attribute instance-attribute","text":"
    code_dark_theme = reactive('material')\n

    The theme to use for code blocks when in dark mode.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.code_light_theme","title":"code_light_theme class-attribute instance-attribute","text":"
    code_light_theme = reactive('material-light')\n

    The theme to use for code blocks when in light mode.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.LinkClicked","title":"LinkClicked","text":"
    LinkClicked(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\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 = unquote(href)\n

    The link that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected","text":"
    TableOfContentsSelected(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 = 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\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\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated","text":"
    TableOfContentsUpdated(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\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\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 = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.goto_anchor","title":"goto_anchor","text":"
    goto_anchor(anchor)\n

    Try and find the given anchor in the current document.

    Parameters:

    Name Type Description Default str

    The anchor to try and find.

    required 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.goto_anchor(anchor)","title":"anchor","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.load","title":"load async","text":"
    load(path)\n

    Load a new Markdown document.

    Parameters:

    Name Type Description Default Path

    Path to the document.

    required

    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.load(path)","title":"path","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    sanitize_location(location)\n

    Given a location, break out the path and any anchor.

    Parameters:

    Name Type Description Default str

    The location to sanitize.

    required

    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.sanitize_location(location)","title":"location","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.unhandled_token","title":"unhandled_token","text":"
    unhandled_token(token)\n

    Process an unhandled token.

    Parameters:

    Name Type Description Default Token

    The MarkdownIt token to handle.

    required

    Returns:

    Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.unhandled_token(token)","title":"token","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.update","title":"update","text":"
    update(markdown)\n

    Update the document with new Markdown.

    Parameters:

    Name Type Description Default str

    A string containing Markdown.

    required

    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.Markdown.update(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock","title":"MarkdownBlock","text":"
    MarkdownBlock(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":"
    action_link(href)\n

    Called on link click.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.build_from_token","title":"build_from_token","text":"
    build_from_token(token)\n

    Build the block content from its source token.

    This method allows the block to be rebuilt on demand, which is useful when the styles assigned to the Markdown.COMPONENT_CLASSES change.

    See https://github.com/Textualize/textual/issues/3464 for more information.

    Parameters:

    Name Type Description Default Token

    The token from which this block is built.

    required"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.build_from_token(token)","title":"token","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\n

    If CSS was reloaded, try to rebuild this block from its token.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.rebuild","title":"rebuild","text":"
    rebuild()\n

    Rebuild the content of the block if we have a source token.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents","title":"MarkdownTableOfContents","text":"
    MarkdownTableOfContents(\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:

    Name Type Description Default Markdown

    The Markdown document associated with this table of contents.

    required str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(disabled)","title":"disabled","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.markdown","title":"markdown instance-attribute","text":"
    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","text":"
    rebuild_table_of_contents(table_of_contents)\n

    Rebuilds the tree representation of the table of contents data.

    Parameters:

    Name Type Description Default TableOfContentsType

    Table of contents.

    required"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.rebuild_table_of_contents(table_of_contents)","title":"table_of_contents","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.watch_table_of_contents","title":"watch_table_of_contents","text":"
    watch_table_of_contents(table_of_contents)\n

    Triggered when the table of contents changes.

    "},{"location":"widgets/masked_input/","title":"MaskedInput","text":"

    Added in version 0.80.0

    A masked input derived from Input, allowing to restrict user input and give visual aid via a simple template mask, which also acts as an implicit validator.

    • Focusable
    • Container
    "},{"location":"widgets/masked_input/#example","title":"Example","text":"

    The example below shows a masked input to ease entering a credit card number.

    Outputcheckbox.py

    MaskedInputApp Enter\u00a0a\u00a0valid\u00a0credit\u00a0card\u00a0number. \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 \u258a0000-0000-0000-0000\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, ComposeResult\nfrom textual.widgets import Label, MaskedInput\n\n\nclass MaskedInputApp(App):\n    # (1)!\n    CSS = \"\"\"\n    MaskedInput.-valid {\n        border: tall $success 60%;\n    }\n    MaskedInput.-valid:focus {\n        border: tall $success;\n    }\n    MaskedInput {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Enter a valid credit card number.\")\n        yield MaskedInput(\n            template=\"9999-9999-9999-9999;0\",  # (2)!\n        )\n\n\napp = MaskedInputApp()\n\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/masked_input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description template str \"\" The template mask string."},{"location":"widgets/masked_input/#the-template-string-format","title":"The template string format","text":"

    A MaskedInput template length defines the maximum length of the input value. Each character of the mask defines a regular expression used to restrict what the user can insert in the corresponding position, and whether the presence of the character in the user input is required for the MaskedInput value to be considered valid, according to the following table:

    Mask character Regular expression Required? A [A-Za-z] Yes a [A-Za-z] No N [A-Za-z0-9] Yes n [A-Za-z0-9] No X [^ ] Yes x [^ ] No 9 [0-9] Yes 0 [0-9] No D [1-9] Yes d [1-9] No # [0-9+\\-] No H [A-Fa-f0-9] Yes h [A-Fa-f0-9] No B [0-1] Yes b [0-1] No

    There are some special characters that can be used to control automatic case conversion during user input: > converts all subsequent user input to uppercase; < to lowercase; ! disables automatic case conversion. Any other character that appears in the template mask is assumed to be a separator, which is a character that is automatically inserted when user reaches its position. All mask characters can be escaped by placing \\ in front of them, allowing any character to be used as separator. The mask can be terminated by ;c, where c is any character you want to be used as placeholder character. The placeholder parameter inherited by Input can be used to override this allowing finer grain tuning of the placeholder string.

    "},{"location":"widgets/masked_input/#messages","title":"Messages","text":"
    • MaskedInput.Changed
    • MaskedInput.Submitted
    "},{"location":"widgets/masked_input/#bindings","title":"Bindings","text":"

    The masked 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/masked_input/#component-classes","title":"Component Classes","text":"

    The masked 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).

    Bases: Input

    A masked text input widget.

    Parameters:

    Name Type Description Default str

    Template string.

    required str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Validator | Iterable[Validator] | None

    An iterable of validators that the MaskedInput value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the masked input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(template)","title":"template","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(value)","title":"value","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(placeholder)","title":"placeholder","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(validators)","title":"validators","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(validate_on)","title":"validate_on","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(valid_empty)","title":"valid_empty","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(name)","title":"name","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(id)","title":"id","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(classes)","title":"classes","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(disabled)","title":"disabled","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(tooltip)","title":"tooltip","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.template","title":"template class-attribute instance-attribute","text":"
    template = template\n

    Input template mask currently in use.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left()\n

    Move the cursor one position to the left; separators are skipped.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_left_word","title":"action_cursor_left_word","text":"
    action_cursor_left_word()\n

    Move the cursor left next to the previous separator. If no previous separator is found, moves the cursor to the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_right","title":"action_cursor_right","text":"
    action_cursor_right()\n

    Move the cursor one position to the right; separators are skipped.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_right_word","title":"action_cursor_right_word","text":"
    action_cursor_right_word()\n

    Move the cursor right next to the next separator. If no next separator is found, moves the cursor to the end of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left","title":"action_delete_left","text":"
    action_delete_left()\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left_all","title":"action_delete_left_all","text":"
    action_delete_left_all()\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left_word","title":"action_delete_left_word","text":"
    action_delete_left_word()\n

    Delete leftward of the cursor position to the previous separator or the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_right","title":"action_delete_right","text":"
    action_delete_right()\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_right_word","title":"action_delete_right_word","text":"
    action_delete_right_word()\n

    Delete the current character and all rightward to next separator or the end of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_home","title":"action_home","text":"
    action_home()\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.clear","title":"clear","text":"
    clear()\n

    Clear the masked input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.insert_text_at_cursor","title":"insert_text_at_cursor","text":"
    insert_text_at_cursor(text)\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters:

    Name Type Description Default str

    New text to insert.

    required"},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.insert_text_at_cursor(text)","title":"text","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.validate","title":"validate","text":"
    validate(value)\n

    Run all the validators associated with this MaskedInput on the supplied value.

    Same as Input.validate() but also validates against template which acts as an additional implicit validator.

    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/masked_input/#textual.widgets.MaskedInput.validate_value","title":"validate_value","text":"
    validate_value(value)\n

    Validates value against template.

    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators.

    Bases: ScrollView

    A vertical option list with bounce-bar highlighting.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\n    \"option-list--option\",\n    \"option-list--option-disabled\",\n    \"option-list--option-highlighted\",\n    \"option-list--option-hover\",\n    \"option-list--option-hover-highlighted\",\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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.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.OptionList.option_count","title":"option_count property","text":"
    option_count\n

    The count of options.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted","title":"OptionHighlighted","text":"
    OptionHighlighted(option_list, index)\n

    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.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage","title":"OptionMessage","text":"
    OptionMessage(option_list, index)\n

    Bases: Message

    Base class for all option messages.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.control","title":"control property","text":"
    control\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.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
    option = get_option_at_index(index)\n

    The highlighted option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
    option_id = id\n

    The ID of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
    option_index = index\n

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
    option_list = option_list\n

    The option list that sent the message.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected","title":"OptionSelected","text":"
    OptionSelected(option_list, index)\n

    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.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Move the highlight down to the next enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Move the highlight up to the previous enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_first","title":"action_first","text":"
    action_first()\n

    Move the highlight to the first enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_last","title":"action_last","text":"
    action_last()\n

    Move the highlight to the last enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the highlight down roughly by one page.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the highlight up roughly by one page.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_select","title":"action_select","text":"
    action_select()\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.OptionList.add_option","title":"add_option","text":"
    add_option(item=None)\n

    Add a new option to the end of the option list.

    Parameters:

    Name Type Description Default NewOptionListContent

    The new item to add.

    None

    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.OptionList.add_option(item)","title":"item","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.add_options","title":"add_options","text":"
    add_options(items)\n

    Add new options to the end of the option list.

    Parameters:

    Name Type Description Default Iterable[NewOptionListContent]

    The new items to add.

    required

    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.OptionList.add_options(items)","title":"items","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.clear_options","title":"clear_options","text":"
    clear_options()\n

    Clear the content of the option list.

    Returns:

    Type Description Self

    The OptionList instance.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option","title":"disable_option","text":"
    disable_option(option_id)\n

    Disable the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to disable.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option_at_index","title":"disable_option_at_index","text":"
    disable_option_at_index(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.OptionList.enable_option","title":"enable_option","text":"
    enable_option(option_id)\n

    Enable the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to enable.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.enable_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.enable_option_at_index","title":"enable_option_at_index","text":"
    enable_option_at_index(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.OptionList.get_option","title":"get_option","text":"
    get_option(option_id)\n

    Get the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to get.

    required

    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.OptionList.get_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.get_option_at_index","title":"get_option_at_index","text":"
    get_option_at_index(index)\n

    Get the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to get.

    required

    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.OptionList.get_option_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.get_option_index","title":"get_option_index","text":"
    get_option_index(option_id)\n

    Get the index of the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to get the index of.

    required

    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.OptionList.get_option_index(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option","title":"remove_option","text":"
    remove_option(option_id)\n

    Remove the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to remove.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option_at_index","title":"remove_option_at_index","text":"
    remove_option_at_index(index)\n

    Remove the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to remove.

    required

    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.OptionList.remove_option_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt","title":"replace_option_prompt","text":"
    replace_option_prompt(option_id, prompt)\n

    Replace the prompt of the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to replace the prompt of.

    required RenderableType

    The new prompt for the option.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index","text":"
    replace_option_prompt_at_index(index, prompt)\n

    Replace the prompt of the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to replace the prompt of.

    required RenderableType

    The new prompt for the option.

    required

    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.OptionList.replace_option_prompt_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt_at_index(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.scroll_to_highlight","title":"scroll_to_highlight","text":"
    scroll_to_highlight(top=False)\n

    Ensure that the highlighted option is in view.

    Parameters:

    Name Type Description Default bool

    Scroll highlight to top of the list.

    False"},{"location":"widgets/option_list/#textual.widgets.OptionList.scroll_to_highlight(top)","title":"top","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.validate_highlighted","title":"validate_highlighted","text":"
    validate_highlighted(highlighted)\n

    Validate the highlighted property value on access.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.watch_highlighted","title":"watch_highlighted","text":"
    watch_highlighted(highlighted)\n

    React to the highlighted option having changed.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID","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","text":"
    Option(prompt, id=None, disabled=False)\n

    Class that holds the details of an individual option.

    Parameters:

    Name Type Description Default RenderableType

    The prompt for the option.

    required str | None

    The optional ID for the option.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"widgets/option_list/#textual.widgets.option_list.Option(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option(id)","title":"id","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option(disabled)","title":"disabled","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option.id","title":"id property","text":"
    id\n

    The optional ID for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option.prompt","title":"prompt property","text":"
    prompt\n

    The prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option.set_prompt","title":"set_prompt","text":"
    set_prompt(prompt)\n

    Set the prompt for the option.

    Parameters:

    Name Type Description Default RenderableType

    The new prompt for the option.

    required"},{"location":"widgets/option_list/#textual.widgets.option_list.Option.set_prompt(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist","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","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.

    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:

    Name Type Description Default str | None

    The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

    None PlaceholderVariant

    The variant of the placeholder.

    'default' str | None

    The name of the placeholder.

    None str | None

    The ID of the placeholder in the DOM.

    None str | None

    A space separated string with the CSS classes of the placeholder, if any.

    None bool

    Whether the placeholder is disabled or not.

    False"},{"location":"widgets/placeholder/#textual.widgets.Placeholder(label)","title":"label","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(variant)","title":"variant","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(name)","title":"name","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(id)","title":"id","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(classes)","title":"classes","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(disabled)","title":"disabled","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder.variant","title":"variant class-attribute instance-attribute","text":"
    variant = validate_variant(variant)\n

    The current variant of the placeholder.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder.cycle_variant","title":"cycle_variant","text":"
    cycle_variant()\n

    Get the next variant in the cycle.

    Returns:

    Type Description Self

    The Placeholder instance.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder.validate_variant","title":"validate_variant","text":"
    validate_variant(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.

    Bases: Widget

    A pretty-printing widget.

    Used to pretty-print any object.

    Parameters:

    Name Type Description Default Any

    The object to pretty-print.

    required str | None

    The name of the pretty widget.

    None str | None

    The ID of the pretty in the DOM.

    None str | None

    The CSS classes of the pretty.

    None"},{"location":"widgets/pretty/#textual.widgets.Pretty(object)","title":"object","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(name)","title":"name","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(id)","title":"id","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(classes)","title":"classes","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty.update","title":"update","text":"
    update(object)\n

    Update the content of the pretty widget.

    Parameters:

    Name Type Description Default Any

    The object to pretty-print.

    required"},{"location":"widgets/pretty/#textual.widgets.Pretty.update(object)","title":"object","text":""},{"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\u00a0Start\u00a0\u258f^p\u00a0palette

    IndeterminateProgressBar \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\u250139%00:00:07 \u00a0s\u00a0Start\u00a0\u258f^p\u00a0palette

    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\u00a0Start\u00a0\u258f^p\u00a0palette

    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/#gradient-bars","title":"Gradient Bars","text":"

    Progress bars support an optional gradient parameter, which renders a smooth gradient rather than a solid bar. To use a gradient, create and set a Gradient object on the ProgressBar widget.

    Note

    Setting a gradient will override styles set in CSS.

    Here's an example:

    Outputprogress_bar_gradient.py

    ProgressApp \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\u250170%--:--:--

    from textual.app import App, ComposeResult\nfrom textual.color import Gradient\nfrom textual.containers import Center, Middle\nfrom textual.widgets import ProgressBar\n\n\nclass ProgressApp(App[None]):\n    \"\"\"Progress bar with a rainbow gradient.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        gradient = Gradient.from_colors(\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        )\n        with Center():\n            with Middle():\n                yield ProgressBar(total=100, gradient=gradient)\n\n    def on_mount(self) -> None:\n        self.query_one(ProgressBar).update(progress=70)\n\n\nif __name__ == \"__main__\":\n    ProgressApp().run()\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\u00a0Start\u00a0\u258f^p\u00a0palette

    StyledProgressBar \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\u250139%00:00:07 \u00a0s\u00a0Start\u00a0\u258f^p\u00a0palette

    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\u00a0Start\u00a0\u258f^p\u00a0palette

    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 progress 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.

    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:

    Name Type Description Default float | None

    The total number of steps in the progress if known.

    None bool

    Whether to show the bar portion of the progress bar.

    True bool

    Whether to show the percentage status of the bar.

    True bool

    Whether to show the ETA countdown of the progress bar.

    True str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False Clock | None

    An optional clock object (leave as default unless testing).

    None Gradient | None

    An optional Gradient object (will replace CSS styles in the bar).

    None"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(total)","title":"total","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_bar)","title":"show_bar","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_percentage)","title":"show_percentage","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_eta)","title":"show_eta","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(name)","title":"name","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(id)","title":"id","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(classes)","title":"classes","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(disabled)","title":"disabled","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(clock)","title":"clock","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(gradient)","title":"gradient","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.gradient","title":"gradient class-attribute instance-attribute","text":"
    gradient = reactive(None)\n

    Optional gradient object (will replace CSS styling in bar).

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.percentage","title":"percentage class-attribute instance-attribute","text":"
    percentage = reactive[Optional[float]](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.ProgressBar.progress","title":"progress class-attribute instance-attribute","text":"
    progress = reactive(0.0)\n

    The progress so far, in number of steps.

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.total","title":"total class-attribute instance-attribute","text":"
    total = 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.ProgressBar.advance","title":"advance","text":"
    advance(advance=1)\n

    Advance the progress of the progress bar by the given amount.

    Example
    progress_bar.advance(10)  # Advance 10 steps.\n

    Parameters:

    Name Type Description Default float

    Number of steps to advance progress by.

    1"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.advance(advance)","title":"advance","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update","title":"update","text":"
    update(*, total=UNUSED, progress=UNUSED, advance=UNUSED)\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:

    Name Type Description Default None | float | UnusedParameter

    New total number of steps.

    UNUSED float | UnusedParameter

    Set the progress to the given number of steps.

    UNUSED float | UnusedParameter

    Advance the progress by this number of steps.

    UNUSED"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(total)","title":"total","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(progress)","title":"progress","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(advance)","title":"advance","text":""},{"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

    Bases: ToggleButton

    A radio button widget that represents a boolean value.

    Note

    A RadioButton is best used within a RadioSet.

    Parameters:

    Name Type Description Default TextType

    The label for the toggle.

    '' bool

    The initial value of the toggle.

    False bool

    Should the button come before the label, or after?

    True str | None

    The name of the toggle.

    None str | None

    The ID of the toggle in the DOM.

    None str | None

    The CSS classes of the toggle.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    RenderableType | None = None,

    None"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(label)","title":"label","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(value)","title":"value","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(button_first)","title":"button_first","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(name)","title":"name","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(id)","title":"id","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(classes)","title":"classes","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(disabled)","title":"disabled","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(tooltip)","title":"tooltip","text":""},{"location":"widgets/radiobutton/#textual.widgets.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.RadioButton.Changed","title":"Changed","text":"
    Changed(toggle_button, value)\n

    Bases: Changed

    Posted when the value of the radio button changes.

    This message can be handled using an on_radio_button_changed method.

    Parameters:

    Name Type Description Default ToggleButton

    The toggle button sending the message.

    required bool

    The value of the toggle button.

    required"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed(toggle_button)","title":"toggle_button","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed(value)","title":"value","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed.control","title":"control property","text":"
    control\n

    Alias for Changed.radio_button.

    "},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed.radio_button","title":"radio_button property","text":"
    radio_button\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

    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:

    Name Type Description Default str | RadioButton

    The labels or RadioButtons to group together.

    () str | None

    The name of the radio set.

    None str | None

    The ID of the radio set in the DOM.

    None str | None

    The CSS classes of the radio set.

    None bool

    Whether the radio set is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None Note

    When a str label is provided, a RadioButton will be created from it.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet(buttons)","title":"buttons","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(name)","title":"name","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(id)","title":"id","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(classes)","title":"classes","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(disabled)","title":"disabled","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(tooltip)","title":"tooltip","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"down,right\",\n        \"next_button\",\n        \"Next option\",\n        show=False,\n    ),\n    Binding(\n        \"enter,space\", \"toggle_button\", \"Toggle\", show=False\n    ),\n    Binding(\n        \"up,left\",\n        \"previous_button\",\n        \"Previous option\",\n        show=False,\n    ),\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.RadioSet.pressed_button","title":"pressed_button property","text":"
    pressed_button\n

    The currently-pressed RadioButton, or None if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet.pressed_index","title":"pressed_index property","text":"
    pressed_index\n

    The index of the currently-pressed RadioButton, or -1 if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet.Changed","title":"Changed","text":"
    Changed(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:

    Name Type Description Default RadioButton

    The radio button that was pressed.

    required"},{"location":"widgets/radioset/#textual.widgets.RadioSet.Changed(pressed)","title":"pressed","text":""},{"location":"widgets/radioset/#textual.widgets.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.RadioSet.Changed.control","title":"control property","text":"
    control\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.RadioSet.Changed.index","title":"index instance-attribute","text":"
    index = pressed_index\n

    The index of the RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets.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.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.RadioSet.action_next_button","title":"action_next_button","text":"
    action_next_button()\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.RadioSet.action_previous_button","title":"action_previous_button","text":"
    action_previous_button()\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.RadioSet.action_toggle_button","title":"action_toggle_button","text":"
    action_toggle_button()\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.

    Bases: ScrollView

    A widget for logging Rich renderables and text.

    Parameters:

    Name Type Description Default int | None

    Maximum number of lines in the log or None for no maximum.

    None int

    Width to use for calls to write with no specified width.

    78 bool

    Enable word wrapping (default is off).

    False bool

    Automatically highlight content. By default, the ReprHighlighter is used. To customize highlighting, set highlight=True and then set the highlighter attribute to an instance of Highlighter.

    False bool

    Apply Rich console markup.

    False bool

    Enable automatic scrolling to end.

    True str | None

    The name of the text log.

    None str | None

    The ID of the text log in the DOM.

    None str | None

    The CSS classes of the text log.

    None bool

    Whether the text log is disabled or not.

    False"},{"location":"widgets/rich_log/#textual.widgets.RichLog(max_lines)","title":"max_lines","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(min_width)","title":"min_width","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(wrap)","title":"wrap","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(highlight)","title":"highlight","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(markup)","title":"markup","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(auto_scroll)","title":"auto_scroll","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(name)","title":"name","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(id)","title":"id","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(classes)","title":"classes","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(disabled)","title":"disabled","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll = auto_scroll\n

    Automatically scroll to the end on write.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.highlight","title":"highlight class-attribute instance-attribute","text":"
    highlight = highlight\n

    Automatically highlight content.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.highlighter","title":"highlighter instance-attribute","text":"
    highlighter = ReprHighlighter()\n

    Rich Highlighter used to highlight content when highlight is True

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.lines","title":"lines instance-attribute","text":"
    lines = []\n

    The lines currently visible in the log.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.markup","title":"markup class-attribute instance-attribute","text":"
    markup = markup\n

    Apply Rich console markup.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines = max_lines\n

    Maximum number of lines in the log or None for no maximum.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.min_width","title":"min_width class-attribute instance-attribute","text":"
    min_width = min_width\n

    Minimum width of renderables.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.wrap","title":"wrap class-attribute instance-attribute","text":"
    wrap = wrap\n

    Enable word wrapping.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.clear","title":"clear","text":"
    clear()\n

    Clear the text log.

    Returns:

    Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.write","title":"write","text":"
    write(\n    content,\n    width=None,\n    expand=False,\n    shrink=True,\n    scroll_end=None,\n)\n

    Write a string or a Rich renderable to the bottom of the log.

    Notes

    The rendering of content will be deferred until the size of the RichLog is known. This means if you call write in compose or on_mount, the content will not be rendered immediately.

    Parameters:

    Name Type Description Default RenderableType | object

    Rich renderable (or a string).

    required int | None

    Width to render, or None to use RichLog.min_width. If specified, expand and shrink will be ignored.

    None bool

    Permit expanding of content to the width of the content region of the RichLog. If width is specified, then expand will be ignored.

    False bool

    Permit shrinking of content to fit within the content region of the RichLog. If width is specified, then shrink will be ignored.

    True bool | None

    Enable automatic scroll to end, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(content)","title":"content","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(width)","title":"width","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(expand)","title":"expand","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(shrink)","title":"shrink","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(scroll_end)","title":"scroll_end","text":""},{"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.

    Bases: Widget

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

    Parameters:

    Name Type Description Default RuleOrientation

    The orientation of the rule.

    'horizontal' LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/rule/#textual.widgets.Rule(orientation)","title":"orientation","text":""},{"location":"widgets/rule/#textual.widgets.Rule(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule(disabled)","title":"disabled","text":""},{"location":"widgets/rule/#textual.widgets.Rule.line_style","title":"line_style class-attribute instance-attribute","text":"
    line_style = line_style\n

    The line style of the rule.

    "},{"location":"widgets/rule/#textual.widgets.Rule.orientation","title":"orientation class-attribute instance-attribute","text":"
    orientation = orientation\n

    The orientation of the rule.

    "},{"location":"widgets/rule/#textual.widgets.Rule.horizontal","title":"horizontal classmethod","text":"
    horizontal(\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:

    Name Type Description Default LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Returns:

    Type Description Rule

    A rule widget with horizontal orientation.

    "},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(disabled)","title":"disabled","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical","title":"vertical classmethod","text":"
    vertical(\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:

    Name Type Description Default LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Returns:

    Type Description Rule

    A rule widget with vertical orientation.

    "},{"location":"widgets/rule/#textual.widgets.Rule.vertical(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(disabled)","title":"disabled","text":""},{"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","text":"

    Bases: Exception

    Exception raised for an invalid rule line style.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation","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.

    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:

    Name Type Description Default Iterable[tuple[RenderableType, SelectType]]

    Options to select from. If no options are provided then allow_blank must be set to True.

    required str

    Text to show in the control when no option is selected.

    'Select' bool

    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.

    True SelectType | NoSelection

    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.

    BLANK str | None

    The name of the select control.

    None str | None

    The ID of the control in the DOM.

    None str | None

    The CSS classes of the control.

    None bool

    Whether the control is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None

    Raises:

    Type Description EmptySelectError

    If no options are provided and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.Select(options)","title":"options","text":""},{"location":"widgets/select/#textual.widgets.Select(prompt)","title":"prompt","text":""},{"location":"widgets/select/#textual.widgets.Select(allow_blank)","title":"allow_blank","text":""},{"location":"widgets/select/#textual.widgets.Select(value)","title":"value","text":""},{"location":"widgets/select/#textual.widgets.Select(name)","title":"name","text":""},{"location":"widgets/select/#textual.widgets.Select(id)","title":"id","text":""},{"location":"widgets/select/#textual.widgets.Select(classes)","title":"classes","text":""},{"location":"widgets/select/#textual.widgets.Select(disabled)","title":"disabled","text":""},{"location":"widgets/select/#textual.widgets.Select(tooltip)","title":"tooltip","text":""},{"location":"widgets/select/#textual.widgets.Select.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"enter,down,space,up\",\n        \"show_overlay\",\n        \"Show menu\",\n        show=False,\n    )\n]\n
    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets.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.expanded","title":"expanded class-attribute instance-attribute","text":"
    expanded = var(False, init=False)\n

    True to show the overlay, otherwise False.

    "},{"location":"widgets/select/#textual.widgets.Select.prompt","title":"prompt class-attribute instance-attribute","text":"
    prompt = prompt\n

    The prompt to show when no value is selected.

    "},{"location":"widgets/select/#textual.widgets.Select.value","title":"value class-attribute instance-attribute","text":"
    value = var[Union[SelectType, NoSelection]](\n    BLANK, init=False\n)\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.Changed","title":"Changed","text":"
    Changed(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.Changed.control","title":"control property","text":"
    control\n

    The Select that sent the message.

    "},{"location":"widgets/select/#textual.widgets.Select.Changed.select","title":"select instance-attribute","text":"
    select = select\n

    The select widget.

    "},{"location":"widgets/select/#textual.widgets.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.action_show_overlay","title":"action_show_overlay","text":"
    action_show_overlay()\n

    Show the overlay.

    "},{"location":"widgets/select/#textual.widgets.Select.clear","title":"clear","text":"
    clear()\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.from_values","title":"from_values classmethod","text":"
    from_values(\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:

    Name Type Description Default Iterable[SelectType]

    Values used to generate options to select from.

    required str

    Text to show in the control when no option is selected.

    'Select' bool

    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.

    True SelectType | NoSelection

    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.

    BLANK str | None

    The name of the select control.

    None str | None

    The ID of the control in the DOM.

    None str | None

    The CSS classes of the control.

    None bool

    Whether the control is disabled or not.

    False

    Returns:

    Type Description Select[SelectType]

    A new Select widget with the provided values as options.

    "},{"location":"widgets/select/#textual.widgets.Select.from_values(values)","title":"values","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(prompt)","title":"prompt","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(allow_blank)","title":"allow_blank","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(value)","title":"value","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(name)","title":"name","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(id)","title":"id","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(classes)","title":"classes","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(disabled)","title":"disabled","text":""},{"location":"widgets/select/#textual.widgets.Select.is_blank","title":"is_blank","text":"
    is_blank()\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.set_options","title":"set_options","text":"
    set_options(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:

    Name Type Description Default Iterable[tuple[RenderableType, SelectType]]

    An iterable of tuples containing the renderable to display for each option and the corresponding internal value.

    required

    Raises:

    Type Description EmptySelectError

    If the options iterable is empty and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.Select.set_options(options)","title":"options","text":""},{"location":"widgets/select/#textual.widgets.select.EmptySelectError","title":"EmptySelectError","text":"

    Bases: Exception

    Raised when a Select has no options and allow_blank=False.

    "},{"location":"widgets/select/#textual.widgets.select.InvalidSelectValueError","title":"InvalidSelectValueError","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 Text) 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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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":"
    • SelectionList.SelectionHighlighted
    • SelectionList.SelectionToggled
    • 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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators.

    Bases: Generic[SelectionType], OptionList

    A vertical selection list that allows making multiple selections.

    Parameters:

    Name Type Description Default Selection[SelectionType] | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

    The content for the selection list.

    () str | None

    The name of the selection list.

    None str | None

    The ID of the selection list in the DOM.

    None str | None

    The CSS classes of the selection list.

    None bool

    Whether the selection list is disabled or not.

    False"},{"location":"widgets/selection_list/#textual.widgets.SelectionList(*selections)","title":"*selections","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(name)","title":"name","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(id)","title":"id","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(classes)","title":"classes","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(disabled)","title":"disabled","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\n    Binding(\"space\", \"select\", \"Toggle option\", show=False)\n]\n
    Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.SelectionList.selected","title":"selected property","text":"
    selected\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.SelectionList.SelectedChanged","title":"SelectedChanged dataclass","text":"
    SelectedChanged(selection_list)\n

    Bases: Generic[MessageSelectionType], Message

    Message sent when the collection of selected values changes.

    This is sent regardless of whether the change occurred via user interaction or programmatically the the SelectionList API.

    When a bulk change occurs, such as through select_all or deselect_all, only a single SelectedChanged message will be sent (rather than one per option).

    Can be handled using on_selection_list_selected_changed in a subclass of SelectionList or in a parent node in the DOM.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectedChanged.control","title":"control property","text":"
    control\n

    An alias for selection_list.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
    selection_list\n

    The SelectionList that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted","title":"SelectionHighlighted","text":"
    SelectionHighlighted(selection_list, index)\n

    Bases: SelectionMessage[MessageSelectionType]

    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.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage","title":"SelectionMessage","text":"
    SelectionMessage(selection_list, index)\n

    Bases: Generic[MessageSelectionType], Message

    Base class for all selection messages.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.control","title":"control property","text":"
    control\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.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
    selection = get_option_at_index(index)\n

    The highlighted selection.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
    selection_index = index\n

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
    selection_list = selection_list\n

    The selection list that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled","title":"SelectionToggled","text":"
    SelectionToggled(selection_list, index)\n

    Bases: SelectionMessage[MessageSelectionType]

    Message sent when a selection is toggled.

    This is only sent when the value is explicitly toggled e.g. via toggle or toggle_all, or via user interaction. If you programmatically set a value to be selected, this message will not be sent, even if it happens to be the opposite of what was originally selected (i.e. setting a True to a False or vice-versa).

    Since this message indicates a toggle occurring at a per-option level, a message will be sent for each option that is toggled, even when a bulk action is performed (e.g. via toggle_all).

    Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.add_option","title":"add_option","text":"
    add_option(item=None)\n

    Add a new selection option to the end of the list.

    Parameters:

    Name Type Description Default NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

    The new item to add.

    None

    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.SelectionList.add_option(item)","title":"item","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.add_options","title":"add_options","text":"
    add_options(items)\n

    Add new selection options to the end of the list.

    Parameters:

    Name Type Description Default Iterable[NewOptionListContent | Selection[SelectionType] | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]]

    The new items to add.

    required

    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.SelectionList.add_options(items)","title":"items","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.clear_options","title":"clear_options","text":"
    clear_options()\n

    Clear the content of the selection list.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect","title":"deselect","text":"
    deselect(selection)\n

    Mark the given selection as not selected.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to mark as not selected.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect_all","title":"deselect_all","text":"
    deselect_all()\n

    Deselect all items.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.get_option","title":"get_option","text":"
    get_option(option_id)\n

    Get the selection option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the selection option to get.

    required

    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.SelectionList.get_option(option_id)","title":"option_id","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.get_option_at_index","title":"get_option_at_index","text":"
    get_option_at_index(index)\n

    Get the selection option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the selection option to get.

    required

    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.SelectionList.get_option_at_index(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select","title":"select","text":"
    select(selection)\n

    Mark the given selection as selected.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to mark as selected.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select_all","title":"select_all","text":"
    select_all()\n

    Select all items.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle","title":"toggle","text":"
    toggle(selection)\n

    Toggle the selected state of the given selection.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to toggle.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle_all","title":"toggle_all","text":"
    toggle_all()\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","text":"
    Selection(\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:

    Name Type Description Default TextType

    The prompt for the selection.

    required SelectionType

    The value for the selection.

    required bool

    The initial selected state of the selection.

    False str | None

    The optional ID for the selection.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(prompt)","title":"prompt","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(value)","title":"value","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(initial_state)","title":"initial_state","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(id)","title":"id","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(disabled)","title":"disabled","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection.initial_state","title":"initial_state property","text":"
    initial_state\n

    The initial selected state for the selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection.value","title":"value property","text":"
    value\n

    The value for this selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError","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 color used for the smaller values in the data.

    Bases: Widget

    A sparkline widget to display numerical data.

    Parameters:

    Name Type Description Default Sequence[float] | None

    The initial data to populate the sparkline with.

    None Callable[[Sequence[float]], float] | None

    Summarizes bar values into a single value used to represent each bar.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/sparkline/#textual.widgets.Sparkline(data)","title":"data","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(summary_function)","title":"summary_function","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(name)","title":"name","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(id)","title":"id","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(classes)","title":"classes","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(disabled)","title":"disabled","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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 color used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets.Sparkline.data","title":"data class-attribute instance-attribute","text":"
    data = data\n

    The data that populates the sparkline.

    "},{"location":"widgets/sparkline/#textual.widgets.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

    Bases: Widget

    A widget to display simple static content, or use as a base class for more complex widgets.

    Parameters:

    Name Type Description Default RenderableType

    A Rich renderable, or string containing console markup.

    '' bool

    Expand content if required to fill container.

    False bool

    Shrink content if required to fill container.

    False bool

    True if markup should be parsed and rendered.

    True str | None

    Name of widget.

    None str | None

    ID of Widget.

    None str | None

    Space separated list of class names.

    None bool

    Whether the static is disabled or not.

    False"},{"location":"widgets/static/#textual.widgets.Static(renderable)","title":"renderable","text":""},{"location":"widgets/static/#textual.widgets.Static(expand)","title":"expand","text":""},{"location":"widgets/static/#textual.widgets.Static(shrink)","title":"shrink","text":""},{"location":"widgets/static/#textual.widgets.Static(markup)","title":"markup","text":""},{"location":"widgets/static/#textual.widgets.Static(name)","title":"name","text":""},{"location":"widgets/static/#textual.widgets.Static(id)","title":"id","text":""},{"location":"widgets/static/#textual.widgets.Static(classes)","title":"classes","text":""},{"location":"widgets/static/#textual.widgets.Static(disabled)","title":"disabled","text":""},{"location":"widgets/static/#textual.widgets.Static.update","title":"update","text":"
    update(renderable='')\n

    Update the widget's content area with new text or Rich renderable.

    Parameters:

    Name Type Description Default RenderableType

    A new rich renderable. Defaults to empty renderable;

    ''"},{"location":"widgets/static/#textual.widgets.Static.update(renderable)","title":"renderable","text":""},{"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;.

    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:

    Name Type Description Default bool

    The initial value of the switch.

    False bool

    True if the switch should animate when toggled.

    True str | None

    The name of the switch.

    None str | None

    The ID of the switch in the DOM.

    None str | None

    The CSS classes of the switch.

    None bool

    Whether the switch is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/switch/#textual.widgets.Switch(value)","title":"value","text":""},{"location":"widgets/switch/#textual.widgets.Switch(animate)","title":"animate","text":""},{"location":"widgets/switch/#textual.widgets.Switch(name)","title":"name","text":""},{"location":"widgets/switch/#textual.widgets.Switch(id)","title":"id","text":""},{"location":"widgets/switch/#textual.widgets.Switch(classes)","title":"classes","text":""},{"location":"widgets/switch/#textual.widgets.Switch(disabled)","title":"disabled","text":""},{"location":"widgets/switch/#textual.widgets.Switch(tooltip)","title":"tooltip","text":""},{"location":"widgets/switch/#textual.widgets.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"enter,space\", \"toggle_switch\", \"Toggle\", show=False\n    )\n]\n
    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {'switch--slider'}\n
    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets.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.Changed","title":"Changed","text":"
    Changed(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.Changed.control","title":"control property","text":"
    control\n

    Alias for self.switch.

    "},{"location":"widgets/switch/#textual.widgets.Switch.action_toggle_switch","title":"action_toggle_switch","text":"
    action_toggle_switch()\n

    Toggle the state of the switch.

    "},{"location":"widgets/switch/#textual.widgets.Switch.toggle","title":"toggle","text":"
    toggle()\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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    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.Cleared
    • 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

    Bases: Widget

    A container with associated tabs to toggle content visibility.

    Parameters:

    Name Type Description Default TextType

    Positional argument will be used as title.

    () str

    The id of the initial tab, or empty string to select the first tab.

    '' str | None

    The name of the tabbed content.

    None str | None

    The ID of the tabbed content in the DOM.

    None str | None

    The CSS classes of the tabbed content.

    None bool

    Whether the tabbed content is disabled or not.

    False

    Bases: Widget

    A container for switchable content, with additional title.

    This widget is intended to be used with TabbedContent.

    Parameters:

    Name Type Description Default TextType

    Title of the TabPane (will be displayed in a tab label).

    required Widget

    Widget to go inside the TabPane.

    () str | None

    Optional name for the TabPane.

    None str | None

    Optional ID for the TabPane.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the TabPane is disabled or not.

    False"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(*titles)","title":"*titles","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(initial)","title":"initial","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(name)","title":"name","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(id)","title":"id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(classes)","title":"classes","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(disabled)","title":"disabled","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.active","title":"active class-attribute instance-attribute","text":"
    active = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.active_pane","title":"active_pane property","text":"
    active_pane\n

    The currently active pane, or None if no pane is active.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.tab_count","title":"tab_count property","text":"
    tab_count\n

    Total number of tabs.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared","title":"Cleared","text":"
    Cleared(tabbed_content)\n

    Bases: Message

    Posted when no tab pane is active.

    This can happen if all tab panes are removed or if the currently active tab pane is unset.

    Parameters:

    Name Type Description Default TabbedContent

    The TabbedContent widget.

    required"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared(tabbed_content)","title":"tabbed_content","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared.control","title":"control property","text":"
    control\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.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.TabbedContent.TabActivated","title":"TabActivated","text":"
    TabActivated(tabbed_content, tab)\n

    Bases: Message

    Posted when the active tab changes.

    Parameters:

    Name Type Description Default TabbedContent

    The TabbedContent widget.

    required ContentTab

    The Tab widget that was selected (contains the tab label).

    required"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.TabActivated(tabbed_content)","title":"tabbed_content","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.TabActivated(tab)","title":"tab","text":""},{"location":"widgets/tabbed_content/#textual.widgets.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.TabbedContent.TabActivated.control","title":"control property","text":"
    control\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.TabbedContent.TabActivated.pane","title":"pane instance-attribute","text":"
    pane = get_pane(tab)\n

    The TabPane widget that was activated by selecting the tab.

    "},{"location":"widgets/tabbed_content/#textual.widgets.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.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.TabbedContent.add_pane","title":"add_pane","text":"
    add_pane(pane, *, before=None, after=None)\n

    Add a new pane to the tabbed content.

    Parameters:

    Name Type Description Default TabPane

    The pane to add.

    required TabPane | str | None

    Optional pane or pane ID to add the pane before.

    None TabPane | str | None

    Optional pane or pane ID to add the pane after.

    None

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be added.

    Raises:

    Type Description 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.TabbedContent.add_pane(pane)","title":"pane","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.add_pane(before)","title":"before","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.add_pane(after)","title":"after","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.clear_panes","title":"clear_panes","text":"
    clear_panes()\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.TabbedContent.disable_tab","title":"disable_tab","text":"
    disable_tab(tab_id)\n

    Disables the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to disable.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.disable_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.enable_tab","title":"enable_tab","text":"
    enable_tab(tab_id)\n

    Enables the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to enable.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.enable_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.get_pane","title":"get_pane","text":"
    get_pane(pane_id)\n

    Get the TabPane associated with the given ID or tab.

    Parameters:

    Name Type Description Default str | ContentTab

    The ID of the pane to get, or the Tab it is associated with.

    required

    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.TabbedContent.get_pane(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.get_tab","title":"get_tab","text":"
    get_tab(pane_id)\n

    Get the Tab associated with the given ID or TabPane.

    Parameters:

    Name Type Description Default str | TabPane

    The ID of the pane, or the pane itself.

    required

    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.TabbedContent.get_tab(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.hide_tab","title":"hide_tab","text":"
    hide_tab(tab_id)\n

    Hides the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to hide.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.hide_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.remove_pane","title":"remove_pane","text":"
    remove_pane(pane_id)\n

    Remove a given pane from the tabbed content.

    Parameters:

    Name Type Description Default str

    The ID of the pane to remove.

    required

    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.TabbedContent.remove_pane(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.show_tab","title":"show_tab","text":"
    show_tab(tab_id)\n

    Shows the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to show.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.show_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(title)","title":"title","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(*children)","title":"*children","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(name)","title":"name","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(id)","title":"id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(classes)","title":"classes","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(disabled)","title":"disabled","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Disabled","title":"Disabled dataclass","text":"
    Disabled(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a tab pane is disabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Enabled","title":"Enabled dataclass","text":"
    Enabled(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a tab pane is enabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Focused","title":"Focused dataclass","text":"
    Focused(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a child widget is focused.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.TabPaneMessage","title":"TabPaneMessage dataclass","text":"
    TabPaneMessage(tab_pane)\n

    Bases: Message

    Base class for TabPane messages.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.TabPaneMessage.control","title":"control property","text":"
    control\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.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
    tab_pane\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\u00a0Add\u00a0tab\u00a0\u00a0r\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0c\u00a0Clear\u00a0tabs\u00a0\u258f^p\u00a0palette

    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.

    Bases: Widget

    A row of tabs.

    Parameters:

    Name Type Description Default Tab | TextType

    Positional argument should be explicit Tab objects, or a str or Text.

    () str | None

    ID of the tab which should be active on start.

    None str | None

    Optional name for the tabs widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Bases: Static

    A Widget to manage a single tab within a Tabs widget.

    Parameters:

    Name Type Description Default TextType

    The label to use in the tab.

    required str | None

    Optional ID for the widget.

    None str | None

    Space separated list of class names.

    None bool

    Whether the tab is disabled or not.

    False"},{"location":"widgets/tabs/#textual.widgets.Tabs(*tabs)","title":"*tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(active)","title":"active","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(name)","title":"name","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(id)","title":"id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(classes)","title":"classes","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(disabled)","title":"disabled","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.active","title":"active class-attribute instance-attribute","text":"
    active = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.active_tab","title":"active_tab property","text":"
    active_tab\n

    The currently active tab, or None if there are no active tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.tab_count","title":"tab_count property","text":"
    tab_count\n

    Total number of tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared","title":"Cleared","text":"
    Cleared(tabs)\n

    Bases: Message

    Sent when there are no active tabs.

    This can occur when Tabs are cleared, if all tabs are hidden, or if the currently active tab is unset.

    Parameters:

    Name Type Description Default Tabs

    The tabs widget.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared.control","title":"control property","text":"
    control\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.Cleared.tabs","title":"tabs instance-attribute","text":"
    tabs = tabs\n

    The tabs widget which was cleared.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated","title":"TabActivated","text":"
    TabActivated(tabs, tab)\n

    Bases: TabMessage

    Sent when a new tab is activated.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled","title":"TabDisabled","text":"
    TabDisabled(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is disabled.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled","title":"TabEnabled","text":"
    TabEnabled(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is enabled.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabError","title":"TabError","text":"

    Bases: Exception

    Exception raised when there is an error relating to tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden","title":"TabHidden","text":"
    TabHidden(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is hidden.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage","title":"TabMessage","text":"
    TabMessage(tabs, tab)\n

    Bases: Message

    Parent class for all messages that have to do with a specific tab.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.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.TabMessage.control","title":"control property","text":"
    control\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.TabMessage.tab","title":"tab instance-attribute","text":"
    tab = tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
    tabs = tabs\n

    The tabs widget containing the tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown","title":"TabShown","text":"
    TabShown(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is shown.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.action_next_tab","title":"action_next_tab","text":"
    action_next_tab()\n

    Make the next tab active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.action_previous_tab","title":"action_previous_tab","text":"
    action_previous_tab()\n

    Make the previous tab active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab","title":"add_tab","text":"
    add_tab(tab, *, before=None, after=None)\n

    Add a new tab to the end of the tab list.

    Parameters:

    Name Type Description Default Tab | str | Text

    A new tab object, or a label (str or Text).

    required Tab | str | None

    Optional tab or tab ID to add the tab before.

    None Tab | str | None

    Optional tab or tab ID to add the tab after.

    None

    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 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.add_tab(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab(before)","title":"before","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab(after)","title":"after","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.clear","title":"clear","text":"
    clear()\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.disable","title":"disable","text":"
    disable(tab_id)\n

    Disable the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to disable.

    required

    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.disable(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.enable","title":"enable","text":"
    enable(tab_id)\n

    Enable the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to enable.

    required

    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.enable(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.hide","title":"hide","text":"
    hide(tab_id)\n

    Hide the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to hide.

    required

    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.hide(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.remove_tab","title":"remove_tab","text":"
    remove_tab(tab_or_id)\n

    Remove a tab.

    Parameters:

    Name Type Description Default Tab | str | None

    The Tab to remove or its id.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be removed.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.remove_tab(tab_or_id)","title":"tab_or_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.show","title":"show","text":"
    show(tab_id)\n

    Show the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to show.

    required

    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.show(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.validate_active","title":"validate_active","text":"
    validate_active(active)\n

    Check id assigned to active attribute is a valid tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.watch_active","title":"watch_active","text":"
    watch_active(previously_active, active)\n

    Handle a change to the active tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab(label)","title":"label","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(id)","title":"id","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(classes)","title":"classes","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(disabled)","title":"disabled","text":""},{"location":"widgets/tabs/#textual.widgets.Tab.label","title":"label property writable","text":"
    label\n

    The label for the tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.label_text","title":"label_text property","text":"
    label_text\n

    Undecorated text of the label.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Clicked","title":"Clicked dataclass","text":"
    Clicked(tab)\n

    Bases: TabMessage

    A tab was clicked.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Disabled","title":"Disabled dataclass","text":"
    Disabled(tab)\n

    Bases: TabMessage

    A tab was disabled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Enabled","title":"Enabled dataclass","text":"
    Enabled(tab)\n

    Bases: TabMessage

    A tab was enabled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Relabelled","title":"Relabelled dataclass","text":"
    Relabelled(tab)\n

    Bases: TabMessage

    A tab was relabelled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.TabMessage","title":"TabMessage dataclass","text":"
    TabMessage(tab)\n

    Bases: Message

    Tab-related messages.

    These are mostly intended for internal use when interacting with Tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.TabMessage.control","title":"control property","text":"
    control\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.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
    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 \u258a1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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

    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.code_editor(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.

    Tip

    The TextArea.document.end property returns the location at the end of the document, which might be convenient when editing programmatically.

    "},{"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 \u258a1\u00a0\u00a0defhello(name):\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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

    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.code_editor(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":"

    Many 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, which takes its 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.

    When using the css theme, you can make use of component classes to style elements of the TextArea. For example, the CSS code TextArea .text-area--cursor { background: green; } will make the cursor green.

    More complex applications such as code editors may 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.code_editor(\"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{'css', '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":"

    Note

    Custom themes are only relevant for people who are looking to customize syntax highlighting. If you're only editing plain text, and wish to recolor aspects of the TextArea, you should use the provided component classes.

    Using custom (non-builtin) themes is a 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-and-escape-behavior","title":"Tab and Escape behavior","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_behavior attribute to the string value \"indent\". While in this mode, you can shift focus by pressing the Esc key.

    "},{"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/#undo-and-redo","title":"Undo and redo","text":"

    TextArea offers undo and redo methods. By default, undo is bound to Ctrl+Z and redo to Ctrl+Y.

    The TextArea uses a heuristic to place checkpoints after certain types of edit. When you call undo, all of the edits between now and the most recent checkpoint are reverted. You can manually add a checkpoint by calling the TextArea.history.checkpoint() instance method.

    The undo and redo history uses a stack-based system, where a single item on the stack represents a single checkpoint. In memory-constrained environments, you may wish to reduce the maximum number of checkpoints that can exist. You can do this by passing the max_checkpoints argument to the TextArea constructor.

    "},{"location":"widgets/text_area/#read-only-mode","title":"Read-only mode","text":"

    TextArea.read_only is a boolean reactive attribute which, if True, will prevent users from modifying content in the TextArea.

    While read_only=True, you can still modify the content programmatically.

    While this mode is active, the TextArea receives the -read-only CSS class, which you can use to supply custom styles for read-only mode.

    "},{"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.

    You can also change the start line number (the topmost line number in the gutter) by setting the line_number_start reactive attribute.

    "},{"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.code_editor(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 \u258a1\u00a0\u00a0def\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.code_editor(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 \u258a1\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\u258e \u258a2\u00a0\u00a0publicstatic\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0\u258e \u258a3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0\u258e \u258a4\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\u00a0\u258e \u258a5\u00a0\u00a0}\u00a0\u258e \u258a6\u00a0\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\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 key-value pair 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 \"css\" The theme to use. selection Selection Selection() The current selection. show_line_numbers bool False Show or hide line numbers. line_number_start int 1 The start line number in the gutter. 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. read_only bool False Enable/disable read-only mode."},{"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 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. ctrl+z Undo. ctrl+y Redo."},{"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
    • EditHistory - manages the undo stack
    • 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.

    Bases: ScrollView

    Parameters:

    Name Type Description Default str

    The initial text to load into the TextArea.

    '' str | None

    The language to use.

    None str

    The theme to use.

    'css' bool

    Enable soft wrapping.

    True Literal['focus', 'indent']

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    'focus' bool

    Enable read-only mode. This prevents edits using the keyboard.

    False bool

    Show line numbers on the left edge.

    False int

    What line number to start on.

    1 int

    The maximum number of undo history checkpoints to retain.

    50 str | None

    The name of the TextArea widget.

    None str | None

    The ID of the widget, used to refer to it from Textual CSS.

    None str | None

    One or more Textual CSS compatible class names separated by spaces.

    None bool

    True if the widget is disabled.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(theme)","title":"theme","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(soft_wrap)","title":"soft_wrap","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(tab_behavior)","title":"tab_behavior","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(read_only)","title":"read_only","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(show_line_numbers)","title":"show_line_numbers","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(line_number_start)","title":"line_number_start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(max_checkpoints)","title":"max_checkpoints","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(id)","title":"id","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(classes)","title":"classes","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(disabled)","title":"disabled","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(tooltip)","title":"tooltip","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\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 character 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 character 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_or_delete_line\",\n        \"Delete to line end\",\n        show=False,\n    ),\n    Binding(\"ctrl+z\", \"undo\", \"Undo\", show=False),\n    Binding(\"ctrl+y\", \"redo\", \"Redo\", show=False),\n]\n
    Key(s) Description 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. ctrl+z Undo. ctrl+y Redo."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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\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\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\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\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\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\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\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\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(True, init=False)\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\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\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 = 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\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.history","title":"history instance-attribute","text":"
    history = EditHistory(\n    max_checkpoints=max_checkpoints,\n    checkpoint_timer=2.0,\n    checkpoint_max_characters=100,\n)\n

    A stack (the end of the list is the top of the stack) for tracking edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
    indent_type = '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(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\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 = 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.line_number_start","title":"line_number_start class-attribute instance-attribute","text":"
    line_number_start = reactive(1, init=False)\n

    The line number the first line should be.

    "},{"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(True, init=False)\n

    If the cursor is at a bracket, highlight the matching bracket (if found).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.matching_bracket_location","title":"matching_bracket_location property","text":"
    matching_bracket_location\n

    The location of the matching bracket, if there is one.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.navigator","title":"navigator instance-attribute","text":"
    navigator = DocumentNavigator(wrapped_document)\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.read_only","title":"read_only class-attribute instance-attribute","text":"
    read_only = reactive(False)\n

    True if the content is read-only.

    Read-only means end users cannot insert, delete or replace content.

    The document can still be edited programmatically via the API.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
    selected_text\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(\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(False, init=False)\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(True, init=False)\n

    True if text should soft wrap.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text property writable","text":"
    text\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 = 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\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(document)\n

    The wrapped view of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed","title":"Changed dataclass","text":"
    Changed(text_area)\n

    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\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\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged","title":"SelectionChanged dataclass","text":"
    SelectionChanged(selection, text_area)\n

    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\n

    The new selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.text_area","title":"text_area instance-attribute","text":"
    text_area\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down(select=False)\n

    Move the cursor down one cell.

    Parameters:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left(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:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end","text":"
    action_cursor_line_end(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","text":"
    action_cursor_line_start(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","text":"
    action_cursor_page_down()\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","text":"
    action_cursor_page_up()\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","text":"
    action_cursor_right(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:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up(select=False)\n

    Move the cursor up one cell.

    Parameters:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left","text":"
    action_cursor_word_left(select=False)\n

    Move the cursor left by a single word, skipping trailing whitespace.

    Parameters:

    Name Type Description Default bool

    Whether to select while moving the cursor.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right","text":"
    action_cursor_word_right(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","text":"
    action_delete_left()\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","text":"
    action_delete_line()\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","text":"
    action_delete_right()\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","text":"
    action_delete_to_end_of_line()\n

    Deletes from the cursor location to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line_or_delete_line","title":"action_delete_to_end_of_line_or_delete_line async","text":"
    action_delete_to_end_of_line_or_delete_line()\n

    Deletes from the cursor location to the end of the line, or deletes the line.

    The line will be deleted if the line is empty.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line","text":"
    action_delete_to_start_of_line()\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","text":"
    action_delete_word_left()\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","text":"
    action_delete_word_right()\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_redo","title":"action_redo","text":"
    action_redo()\n

    Redo the most recently undone batch of edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all","text":"
    action_select_all()\n

    Select all the text in the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line","text":"
    action_select_line()\n

    Select all the text on the current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_undo","title":"action_undo","text":"
    action_undo()\n

    Undo the edits since the last checkpoint (the most recent batch of edits).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index","text":"
    cell_width_to_column_index(cell_width, row_index)\n

    Return the column that the cell width corresponds to on the given row.

    Parameters:

    Name Type Description Default int

    The cell width to convert.

    required int

    The index of the row to examine.

    required

    Returns:

    Type Description int

    The column corresponding to the cell width on that row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index(cell_width)","title":"cell_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index(row_index)","title":"row_index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character=None)\n

    Check if the widget may consume the given key.

    As a textarea we are expecting to capture printable keys.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    None

    Returns:

    Type Description bool

    True if the widget may capture the key in it's Key message, or False if it won't.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key(key)","title":"key","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key(character)","title":"character","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable","text":"
    clamp_visitable(location)\n

    Clamp the given location to the nearest visitable location.

    Parameters:

    Name Type Description Default Location

    The location to clamp.

    required

    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.clamp_visitable(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear","text":"
    clear()\n

    Delete all text from the document.

    Returns:

    Type Description EditResult

    An EditResult relating to the deletion of all content.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor","title":"code_editor classmethod","text":"
    code_editor(\n    text=\"\",\n    *,\n    language=None,\n    theme=\"monokai\",\n    soft_wrap=False,\n    tab_behavior=\"indent\",\n    read_only=False,\n    show_line_numbers=True,\n    line_number_start=1,\n    max_checkpoints=50,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    tooltip=None\n)\n

    Construct a new TextArea with sensible defaults for editing code.

    This instantiates a TextArea with line numbers enabled, soft wrapping disabled, \"indent\" tab behavior, and the \"monokai\" theme.

    Parameters:

    Name Type Description Default str

    The initial text to load into the TextArea.

    '' str | None

    The language to use.

    None str

    The theme to use.

    'monokai' bool

    Enable soft wrapping.

    False Literal['focus', 'indent']

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    'indent' bool

    Show line numbers on the left edge.

    True int

    What line number to start on.

    1 str | None

    The name of the TextArea widget.

    None str | None

    The ID of the widget, used to refer to it from Textual CSS.

    None str | None

    One or more Textual CSS compatible class names separated by spaces.

    None bool

    True if the widget is disabled.

    False RenderableType | None

    Optional tooltip

    None"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(theme)","title":"theme","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(soft_wrap)","title":"soft_wrap","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(tab_behavior)","title":"tab_behavior","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(show_line_numbers)","title":"show_line_numbers","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(line_number_start)","title":"line_number_start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(id)","title":"id","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(classes)","title":"classes","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(disabled)","title":"disabled","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(tooltip)","title":"tooltip","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete","text":"
    delete(start, end, *, maintain_selection_offset=True)\n

    Delete the text between two locations in the document.

    Parameters:

    Name Type Description Default Location

    The start location.

    required Location

    The end location.

    required bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit","text":"
    edit(edit)\n

    Perform an Edit.

    Parameters:

    Name Type Description Default Edit

    The Edit to perform.

    required

    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.edit(edit)","title":"edit","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket","text":"
    find_matching_bracket(bracket, search_from)\n

    If the character is a bracket, find the matching bracket.

    Parameters:

    Name Type Description Default str

    The character we're searching for the matching bracket of.

    required Location

    The location to start the search.

    required

    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.find_matching_bracket(bracket)","title":"bracket","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket(search_from)","title":"search_from","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width","text":"
    get_column_width(row, column)\n

    Get the cell offset of the column from the start of the row.

    Parameters:

    Name Type Description Default int

    The row index.

    required int

    The column index (codepoint offset from start of row).

    required

    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_column_width(row)","title":"row","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width(column)","title":"column","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location","text":"
    get_cursor_down_location()\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","text":"
    get_cursor_left_location()\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","text":"
    get_cursor_line_end_location()\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","text":"
    get_cursor_line_start_location(smart_home=False)\n

    Get the location of the start of the current line.

    Parameters:

    Name Type Description Default bool

    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.

    False

    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_line_start_location(smart_home)","title":"smart_home","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location","text":"
    get_cursor_right_location()\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","text":"
    get_cursor_up_location()\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","text":"
    get_cursor_word_left_location()\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","text":"
    get_cursor_word_right_location()\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_line","title":"get_line","text":"
    get_line(line_index)\n

    Retrieve the line at the given line index.

    You can stylize the Text object returned here to apply additional styling to TextArea content.

    Parameters:

    Name Type Description Default int

    The index of the line.

    required

    Returns:

    Type Description Text

    A rich.Text object containing the requested line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_line(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location","text":"
    get_target_document_location(event)\n

    Given a MouseEvent, return the row and column offset of the event in document-space.

    Parameters:

    Name Type Description Default MouseEvent

    The MouseEvent.

    required

    Returns:

    Type Description Location

    The location of the mouse event within the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location(event)","title":"event","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range","text":"
    get_text_range(start, end)\n

    Get the text between a start and end location.

    Parameters:

    Name Type Description Default Location

    The start location.

    required Location

    The end location.

    required

    Returns:

    Type Description str

    The text between start and end.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert","text":"
    insert(\n    text, location=None, *, maintain_selection_offset=True\n)\n

    Insert text into the document.

    Parameters:

    Name Type Description Default str

    The text to insert.

    required Location | None

    The location to insert text, or None to use the cursor location.

    None bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text","text":"
    load_text(text)\n

    Load text into the TextArea.

    This will replace the text currently in the TextArea and clear the edit history.

    Parameters:

    Name Type Description Default str

    The text to load into the TextArea.

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor","text":"
    move_cursor(\n    location, select=False, center=False, record_width=True\n)\n

    Move the cursor to a location.

    Parameters:

    Name Type Description Default Location

    The location to move the cursor to.

    required bool

    If True, select text between the old and new location.

    False bool

    If True, scroll such that the cursor is centered.

    False bool

    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.

    True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(record_width)","title":"record_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative","text":"
    move_cursor_relative(\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:

    Name Type Description Default int

    The number of rows to move down by (negative to move up)

    0 int

    The number of columns to move right by (negative to move left)

    0 bool

    If True, select text between the old and new location.

    False bool

    If True, scroll such that the cursor is centered.

    False bool

    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.

    True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(rows)","title":"rows","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(columns)","title":"columns","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(record_width)","title":"record_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width","text":"
    record_cursor_width()\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.redo","title":"redo","text":"
    redo()\n

    Redo the most recently undone batch of edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language","text":"
    register_language(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:

    Name Type Description Default 'str | Language'

    A string referring to a builtin language or a tree-sitter Language object.

    required str

    The highlight query to use for syntax highlighting this language.

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language(highlight_query)","title":"highlight_query","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme","text":"
    register_theme(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","text":"
    replace(\n    insert, start, end, *, maintain_selection_offset=True\n)\n

    Replace text in the document with new text.

    Parameters:

    Name Type Description Default str

    The text to insert.

    required Location

    The start location

    required Location

    The end location.

    required bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(insert)","title":"insert","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible","text":"
    scroll_cursor_visible(center=False, animate=False)\n

    Scroll the TextArea such that the cursor is visible on screen.

    Parameters:

    Name Type Description Default bool

    True if the cursor should be scrolled to the center.

    False bool

    True if we should animate while scrolling.

    False

    Returns:

    Type Description Offset

    The offset that was scrolled to bring the cursor into view.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible(animate)","title":"animate","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all","text":"
    select_all()\n

    Select all of the text in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line","text":"
    select_line(index)\n

    Select all the text in the specified line.

    Parameters:

    Name Type Description Default int

    The index of the line to select (starting from 0).

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.undo","title":"undo","text":"
    undo()\n

    Undo the edits since the last checkpoint (the most recent batch of edits).

    "},{"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","text":"
    Document(text)\n

    Bases: DocumentBase

    A document which can be opened in a TextArea.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.end","title":"end property","text":"
    end\n

    Returns the location of the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.line_count","title":"line_count property","text":"
    line_count\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.lines","title":"lines property","text":"
    lines\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.widgets.text_area.Document.newline","title":"newline property","text":"
    newline\n

    Get the Newline used in this document (e.g. ' ', ' '. etc.)

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.start","title":"start property","text":"
    start\n

    Returns the location of the start of the document (0, 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.text","title":"text property","text":"
    text\n

    Get the text from the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_index_from_location","title":"get_index_from_location","text":"
    get_index_from_location(location)\n

    Given a location, returns the index from the document's text.

    Parameters:

    Name Type Description Default Location

    The location in the document.

    required

    Returns:

    Type Description int

    The index in the document's text.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_index_from_location(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_line","title":"get_line","text":"
    get_line(index)\n

    Returns the line with the given index from the document.

    Parameters:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_location_from_index","title":"get_location_from_index","text":"
    get_location_from_index(index)\n

    Given an index in the document's text, returns the corresponding location.

    Parameters:

    Name Type Description Default int

    The index in the document's text.

    required

    Returns:

    Type Description Location

    The corresponding location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_location_from_index(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_size","title":"get_size","text":"
    get_size(tab_width)\n

    The Size of the document, taking into account the tab rendering width.

    Parameters:

    Name Type Description Default int

    The width to use for tab indents.

    required

    Returns:

    Type Description Size

    The size (width, height) of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_size(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range","title":"get_text_range","text":"
    get_text_range(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:

    Name Type Description Default Location

    The start location of the selection.

    required Location

    The end location of the selection.

    required

    Returns:

    Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range","title":"replace_range","text":"
    replace_range(start, end, text)\n

    Replace text at the given range.

    This is the only method by which a document may be updated.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The EditResult containing information about the completed replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase","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.widgets.text_area.DocumentBase.end","title":"end abstractmethod property","text":"
    end\n

    Returns the location of the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.line_count","title":"line_count abstractmethod property","text":"
    line_count\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.lines","title":"lines abstractmethod property","text":"
    lines\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.widgets.text_area.DocumentBase.newline","title":"newline abstractmethod property","text":"
    newline\n

    Return the line separator used in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.start","title":"start abstractmethod property","text":"
    start\n

    Returns the location of the start of the document (0, 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.text","title":"text abstractmethod property","text":"
    text\n

    The text from the document as a string.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_line","title":"get_line abstractmethod","text":"
    get_line(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:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description str

    The str instance representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_size","title":"get_size abstractmethod","text":"
    get_size(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:

    Name Type Description Default int

    The width to use for tab characters.

    required

    Returns:

    Type Description Size

    The Size of the document bounding box.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_size(indent_width)","title":"indent_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
    get_text_range(start, end)\n

    Get the text that falls between the start and end locations.

    Parameters:

    Name Type Description Default Location

    The start location of the selection.

    required Location

    The end location of the selection.

    required

    Returns:

    Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree","title":"query_syntax_tree","text":"
    query_syntax_tree(query, start_point=None, end_point=None)\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:

    Name Type Description Default Query

    The tree-sitter Query to perform.

    required tuple[int, int] | None

    The (row, column byte) to start the query at.

    None tuple[int, int] | None

    The (row, column byte) to end the query at.

    None

    Returns:

    Type Description list[tuple[Node, str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(start_point)","title":"start_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(end_point)","title":"end_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
    replace_range(start, end, text)\n

    Replace the text at the given range.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator","title":"DocumentNavigator","text":"
    DocumentNavigator(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 behavior.

    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 behavior. 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 behavior applies. This is designed to make cursor movement more useful to end users.

    Parameters:

    Name Type Description Default WrappedDocument

    The WrappedDocument to be used when making navigation decisions.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator(wrapped_document)","title":"wrapped_document","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.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.widgets.text_area.DocumentNavigator.clamp_reachable","title":"clamp_reachable","text":"
    clamp_reachable(location)\n

    Given a location, return the nearest location that corresponds to a reachable location in the document.

    Parameters:

    Name Type Description Default Location

    A location.

    required

    Returns:

    Type Description Location

    The nearest reachable location in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.clamp_reachable(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_above","title":"get_location_above","text":"
    get_location_above(location)\n

    Get the location visually aligned with the cell above the given location.

    Parameters:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The cell above the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_above(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset","title":"get_location_at_y_offset","text":"
    get_location_at_y_offset(location, vertical_offset)\n

    Apply a visual vertical offset to a location and check the resulting location.

    Parameters:

    Name Type Description Default Location

    The location to start from.

    required int

    The vertical offset to move (negative=up, positive=down).

    required

    Returns:

    Type Description Location

    The location after the offset has been applied.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset(vertical_offset)","title":"vertical_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_below","title":"get_location_below","text":"
    get_location_below(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:

    Name Type Description Default Location

    The location in the raw document.

    required

    Returns:

    Type Description Location

    The location which is visually below the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_below(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_end","title":"get_location_end","text":"
    get_location_end(location)\n

    Get the location corresponding to the end of the current section.

    Parameters:

    Name Type Description Default Location

    The current location.

    required

    Returns:

    Type Description Location

    The location corresponding to the end of the wrapped line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_end(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home","title":"get_location_home","text":"
    get_location_home(location, smart_home=False)\n

    Get the \"home location\" corresponding to the given location.

    Parameters:

    Name Type Description Default Location

    The location to consider.

    required bool

    Enable/disable 'smart home' behavior.

    False

    Returns:

    Type Description Location

    The home location, relative to the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home(smart_home)","title":"smart_home","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_left","title":"get_location_left","text":"
    get_location_left(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:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_left(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_right","title":"get_location_right","text":"
    get_location_right(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:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_right(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document","title":"is_end_of_document","text":"
    is_end_of_document(location)\n

    Check if a location is at the end of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is at the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document_line","title":"is_end_of_document_line","text":"
    is_end_of_document_line(location)\n

    True if the location is at the end of a line in the document.

    Note that the \"end\" of a line is equal to its length (one greater than the final index), since there is a space at the end of the line for the cursor to rest.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the document is at the end of a line in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_wrapped_line","title":"is_end_of_wrapped_line","text":"
    is_end_of_wrapped_line(location)\n

    True if the location is at the end of a wrapped line.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_end_of_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_document_line","title":"is_first_document_line","text":"
    is_first_document_line(location)\n

    Check if the given location is on the first line in the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is on the first line of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_wrapped_line","title":"is_first_wrapped_line","text":"
    is_first_wrapped_line(location)\n

    Check if the given location is on the first wrapped section of the first line in the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_first_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_document_line","title":"is_last_document_line","text":"
    is_last_document_line(location)\n

    Check if the given location is on the last line of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True when the location is on the last line of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_wrapped_line","title":"is_last_wrapped_line","text":"
    is_last_wrapped_line(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:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_last_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document","title":"is_start_of_document","text":"
    is_start_of_document(location)\n

    Check if a location is at the start of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is at document location (0, 0)

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document_line","title":"is_start_of_document_line","text":"
    is_start_of_document_line(location)\n

    True when the location is at the start of the first document line.

    Parameters:

    Name Type Description Default Location

    The location to check.

    required

    Returns:

    Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_wrapped_line","title":"is_start_of_wrapped_line","text":"
    is_start_of_wrapped_line(location)\n

    True when the location is at the start of the first wrapped line.

    Parameters:

    Name Type Description Default Location

    The location to check.

    required

    Returns:

    Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit dataclass","text":"
    Edit(\n    text,\n    from_location,\n    to_location,\n    maintain_selection_offset,\n)\n

    Implements the Undoable protocol to replace text at some range within a document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.bottom","title":"bottom property","text":"
    bottom\n

    The Location impacted by this edit that is nearest the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.from_location","title":"from_location instance-attribute","text":"
    from_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\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\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\n

    The end location of the insert

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.top","title":"top property","text":"
    top\n

    The Location impacted by this edit that is nearest the start of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.after","title":"after","text":"
    after(text_area)\n

    Hook for running code after an Edit has been performed via Edit.do and side effects such as re-wrapping the document and refreshing the display have completed.

    For example, we can't record cursor visual offset until we know where the cursor will land after wrapping has been performed, so we must wait until here to do it.

    Parameters:

    Name Type Description Default TextArea

    The TextArea this operation was performed on.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.after(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do","title":"do","text":"
    do(text_area, record_selection=True)\n

    Perform the edit operation.

    Parameters:

    Name Type Description Default TextArea

    The TextArea to perform the edit on.

    required bool

    If True, record the current selection in the TextArea so that it may be restored if this Edit is undone in the future.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do(record_selection)","title":"record_selection","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.undo","title":"undo","text":"
    undo(text_area)\n

    Undo the edit operation.

    Looks at the data stored in the edit, and performs the inverse operation of Edit.do.

    Parameters:

    Name Type Description Default TextArea

    The TextArea to undo the insert operation on.

    required

    Returns:

    Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.undo(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory","title":"EditHistory dataclass","text":"
    EditHistory(\n    max_checkpoints,\n    checkpoint_timer,\n    checkpoint_max_characters,\n)\n

    Manages batching/checkpointing of Edits into groups that can be undone/redone in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint_max_characters","title":"checkpoint_max_characters instance-attribute","text":"
    checkpoint_max_characters\n

    Maximum number of characters that can appear in a batch before a new batch is formed.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint_timer","title":"checkpoint_timer instance-attribute","text":"
    checkpoint_timer\n

    Maximum number of seconds since last edit until a new batch is created.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.redo_stack","title":"redo_stack property","text":"
    redo_stack\n

    A copy of the redo stack, with references to the original Edits.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.undo_stack","title":"undo_stack property","text":"
    undo_stack\n

    A copy of the undo stack, with references to the original Edits.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint","title":"checkpoint","text":"
    checkpoint()\n

    Ensure the next recorded edit starts a new batch.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.clear","title":"clear","text":"
    clear()\n

    Completely clear the history.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.record","title":"record","text":"
    record(edit)\n

    Record an Edit so that it may be undone and redone.

    Determines whether to batch the Edit with previous Edits, or create a new batch/checkpoint.

    This method must be called exactly once per edit, in chronological order.

    A new batch/checkpoint is created when:

    • The undo stack is empty.
    • The checkpoint timer expires.
    • The maximum number of characters permitted in a checkpoint is reached.
    • A redo is performed (we should not add new edits to a batch that has been redone).
    • The programmer has requested a new batch via a call to force_new_batch.
      • e.g. the TextArea widget may call this method in some circumstances.
      • Clicking to move the cursor elsewhere in the document should create a new batch.
      • Movement of the cursor via a keyboard action that is NOT an edit.
      • Blurring the TextArea creates a new checkpoint.
    • The current edit involves a deletion/replacement and the previous edit did not.
    • The current edit is a pure insertion and the previous edit was not.
    • The edit involves insertion or deletion of one or more newline characters.
    • An edit which inserts more than a single character (a paste) gets an isolated batch.

    Parameters:

    Name Type Description Default Edit

    The edit to record.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.record(edit)","title":"edit","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult dataclass","text":"
    EditResult(end_location, replaced_text)\n

    Contains information about an edit that has occurred.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult.end_location","title":"end_location instance-attribute","text":"
    end_location\n

    The new end Location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
    replaced_text\n

    The text that was replaced.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist","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","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.widgets.text_area.Selection.end","title":"end class-attribute instance-attribute","text":"
    end = (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.widgets.text_area.Selection.is_empty","title":"is_empty property","text":"
    is_empty\n

    Return True if the selection has 0 width, i.e. it's just a cursor.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Selection.start","title":"start class-attribute instance-attribute","text":"
    start = (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.widgets.text_area.Selection.cursor","title":"cursor classmethod","text":"
    cursor(location)\n

    Create a Selection with the same start and end point - a \"cursor\".

    Parameters:

    Name Type Description Default Location

    The location to create the zero-width Selection.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.Selection.cursor(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument","text":"
    SyntaxAwareDocument(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:

    Name Type Description Default str

    The initial text contained in the document.

    required str | Language

    The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
    language = get_language(language)\n

    The tree-sitter Language or None if tree-sitter is unavailable.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.get_line","title":"get_line","text":"
    get_line(line_index)\n

    Return the string representing the line, not including new line characters.

    Parameters:

    Name Type Description Default int

    The index of the line.

    required

    Returns:

    Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.get_line(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.prepare_query","title":"prepare_query","text":"
    prepare_query(query)\n

    Prepare a tree-sitter tree query.

    Queries should be prepared once, then reused.

    To execute a query, call query_syntax_tree.

    Parameters:

    Name Type Description Default str

    The string query to prepare.

    required

    Returns:

    Type Description Query | None

    The prepared query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.prepare_query(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree","text":"
    query_syntax_tree(query, start_point=None, end_point=None)\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:

    Name Type Description Default Query

    The tree-sitter Query to perform.

    required tuple[int, int] | None

    The (row, column byte) to start the query at.

    None tuple[int, int] | None

    The (row, column byte) to end the query at.

    None

    Returns:

    Type Description list[tuple['Node', str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(start_point)","title":"start_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(end_point)","title":"end_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range","title":"replace_range","text":"
    replace_range(start, end, text)\n

    Replace text at the given range.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme dataclass","text":"
    TextAreaTheme(\n    name,\n    base_style=None,\n    gutter_style=None,\n    cursor_style=None,\n    cursor_line_style=None,\n    cursor_line_gutter_style=None,\n    bracket_matching_style=None,\n    selection_style=None,\n    syntax_styles=dict(),\n)\n

    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.widgets.text_area.TextAreaTheme.base_style","title":"base_style class-attribute instance-attribute","text":"
    base_style = None\n

    The background style of the text area. If None the parent style will be used.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style class-attribute instance-attribute","text":"
    bracket_matching_style = None\n

    The style to apply to matching brackets. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style class-attribute instance-attribute","text":"
    cursor_line_gutter_style = 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.widgets.text_area.TextAreaTheme.cursor_line_style","title":"cursor_line_style class-attribute instance-attribute","text":"
    cursor_line_style = None\n

    The style to apply to the line the cursor is on.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.cursor_style","title":"cursor_style class-attribute instance-attribute","text":"
    cursor_style = None\n

    The style of the cursor. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.gutter_style","title":"gutter_style class-attribute instance-attribute","text":"
    gutter_style = None\n

    The style of the gutter. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.name","title":"name instance-attribute","text":"
    name\n

    The name of the theme.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.selection_style","title":"selection_style class-attribute instance-attribute","text":"
    selection_style = None\n

    The style of the selection. If None a default selection Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.syntax_styles","title":"syntax_styles class-attribute instance-attribute","text":"
    syntax_styles = field(default_factory=dict)\n

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.apply_css","title":"apply_css","text":"
    apply_css(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:

    Name Type Description Default TextArea

    The TextArea instance to retrieve fallback styling from.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.apply_css(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
    builtin_themes()\n

    Get a list of all builtin TextAreaThemes.

    Returns:

    Type Description list[TextAreaTheme]

    A list of all builtin TextAreaThemes.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
    get_builtin_theme(theme_name)\n

    Get a TextAreaTheme by name.

    Given a theme_name, return the corresponding TextAreaTheme object.

    Parameters:

    Name Type Description Default str

    The name of the theme.

    required

    Returns:

    Type Description TextAreaTheme | None

    The TextAreaTheme corresponding to the name or None if the theme isn't found.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_builtin_theme(theme_name)","title":"theme_name","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_highlight","title":"get_highlight","text":"
    get_highlight(name)\n

    Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

    Parameters:

    Name Type Description Default str

    The name of the highlight.

    required

    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.TextAreaTheme.get_highlight(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist","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","text":"
    WrappedDocument(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:

    Name Type Description Default DocumentBase

    The document to wrap.

    required int

    The width to wrap at.

    0 int

    The maximum width to consider for tab characters.

    4"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(document)","title":"document","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(width)","title":"width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.document","title":"document instance-attribute","text":"
    document = document\n

    The document wrapping is performed on.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.height","title":"height property","text":"
    height\n

    The height of the wrapped document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.lines","title":"lines property","text":"
    lines\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.widgets.text_area.WrappedDocument.wrapped","title":"wrapped property","text":"
    wrapped\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.widgets.text_area.WrappedDocument.get_offsets","title":"get_offsets","text":"
    get_offsets(line_index)\n

    Given a line index, get the offsets within that line where wrapping should occur for the current document.

    Parameters:

    Name Type Description Default int

    The index of the line within the document.

    required

    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.widgets.text_area.WrappedDocument.get_offsets(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_sections","title":"get_sections","text":"
    get_sections(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:

    Name Type Description Default int

    The index of the line to get sections for.

    required

    Returns:

    Type Description list[str]

    The wrapped line as a list of strings.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_sections(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_tab_widths","title":"get_tab_widths","text":"
    get_tab_widths(line_index)\n

    Return a list of the tab widths for the given line index.

    Parameters:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description list[int]

    An ordered list of the expanded width of the tabs in the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_tab_widths(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column","title":"get_target_document_column","text":"
    get_target_document_column(line_index, x_offset, y_offset)\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:

    Name Type Description Default int

    The index of the line in the document.

    required int

    The x-offset within the wrapped line.

    required int

    The y-offset within the wrapped line (supports negative indexing).

    required

    Returns:

    Type Description int

    The column index corresponding to the line index and y offset.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(x_offset)","title":"x_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(y_offset)","title":"y_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.location_to_offset","title":"location_to_offset","text":"
    location_to_offset(location)\n

    Convert a location in the document to an offset within the wrapped/visual display of the document.

    Parameters:

    Name Type Description Default Location

    The location in the document.

    required

    Returns:

    Type Description Offset

    The Offset in the document's visual display corresponding to the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.location_to_offset(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.offset_to_location","title":"offset_to_location","text":"
    offset_to_location(offset)\n

    Given an offset within the wrapped/visual display of the document, return the corresponding location in the document.

    Parameters:

    Name Type Description Default Offset

    The y-offset within the document.

    required

    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.widgets.text_area.WrappedDocument.offset_to_location(offset)","title":"offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap","title":"wrap","text":"
    wrap(width, tab_width=None)\n

    Wrap and cache all lines in the document.

    Parameters:

    Name Type Description Default int

    The width to wrap at. 0 for no wrapping.

    required int | None

    The maximum width to consider for tab characters. If None, reuse the tab width.

    None"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap(width)","title":"width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range","title":"wrap_range","text":"
    wrap_range(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:

    Name Type Description Default Location

    The start location of the edit that was performed in document-space.

    required Location

    The old end location of the edit in document-space.

    required Location

    The new end location of the edit in document-space.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(old_end)","title":"old_end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(new_end)","title":"new_end","text":""},{"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

    If you wish to change the location of Toasts, it is possible by targeting the ToastRack CSS type. For example:

    ToastRack {\n        align: right top;\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 \u258cIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0 \u258cchecks\u00a0out. \u258c \u258c \u258cPossible\u00a0trap\u00a0detected \u258cNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u00a0 \u258cfully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u00a0battle\u00a0 \u258cstation! \u258c \u258c \u258cIt's\u00a0a\u00a0trap! \u258c \u258c \u258cIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0 \u258cimpersonate\u00a0a\u00a0deity. \u258c

    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","text":"
    Toast(notification)\n

    Bases: Static

    A widget for displaying short-lived notifications.

    Parameters:

    Name Type Description Default Notification

    The notification to show in the toast.

    required"},{"location":"widgets/toast/#textual.widgets._toast.Toast(notification)","title":"notification","text":""},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {'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.

    Bases: Generic[TreeDataType], ScrollView

    A widget for displaying and navigating data in a tree.

    Parameters:

    Name Type Description Default TextType

    The label of the root node of the tree.

    required TreeDataType | None

    The optional data to associate with the root node of the tree.

    None str | None

    The name of the Tree.

    None str | None

    The ID of the tree in the DOM.

    None str | None

    The CSS classes of the tree.

    None bool

    Whether the tree is disabled or not.

    False

    Make non-widget Tree support classes available.

    "},{"location":"widgets/tree/#textual.widgets.Tree(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.Tree(name)","title":"name","text":""},{"location":"widgets/tree/#textual.widgets.Tree(id)","title":"id","text":""},{"location":"widgets/tree/#textual.widgets.Tree(classes)","title":"classes","text":""},{"location":"widgets/tree/#textual.widgets.Tree(disabled)","title":"disabled","text":""},{"location":"widgets/tree/#textual.widgets.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"shift+left\",\n        \"cursor_parent\",\n        \"Cursor to parent\",\n        show=False,\n    ),\n    Binding(\n        \"shift+right\",\n        \"cursor_parent_next_sibling\",\n        \"Cursor to next ancestor\",\n        show=False,\n    ),\n    Binding(\n        \"shift+up\",\n        \"cursor_previous_sibling\",\n        \"Cursor to previous sibling\",\n        show=False,\n    ),\n    Binding(\n        \"shift+down\",\n        \"cursor_next_sibling\",\n        \"Cursor to next sibling\",\n        show=False,\n    ),\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"space\", \"toggle_node\", \"Toggle\", show=False),\n    Binding(\n        \"shift+space\",\n        \"toggle_expand_all\",\n        \"Expand or collapse all\",\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]\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.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.ICON_NODE","title":"ICON_NODE class-attribute instance-attribute","text":"
    ICON_NODE = '\u25b6 '\n

    Unicode 'icon' to use for an expandable node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.ICON_NODE_EXPANDED","title":"ICON_NODE_EXPANDED class-attribute instance-attribute","text":"
    ICON_NODE_EXPANDED = '\u25bc '\n

    Unicode 'icon' to use for an expanded node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.auto_expand","title":"auto_expand class-attribute instance-attribute","text":"
    auto_expand = var(True)\n

    Auto expand tree nodes when they are selected.

    "},{"location":"widgets/tree/#textual.widgets.Tree.center_scroll","title":"center_scroll class-attribute instance-attribute","text":"
    center_scroll = var(False)\n

    Keep selected node in the center of the control, where possible.

    "},{"location":"widgets/tree/#textual.widgets.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.cursor_node","title":"cursor_node property","text":"
    cursor_node\n

    The currently selected node, or None if no selection.

    "},{"location":"widgets/tree/#textual.widgets.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.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.last_line","title":"last_line property","text":"
    last_line\n

    The index of the last line.

    "},{"location":"widgets/tree/#textual.widgets.Tree.root","title":"root instance-attribute","text":"
    root = _add_node(None, text_label, data)\n

    The root node of the tree.

    "},{"location":"widgets/tree/#textual.widgets.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.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.NodeCollapsed","title":"NodeCollapsed","text":"
    NodeCollapsed(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.NodeCollapsed.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was collapsed.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeExpanded","title":"NodeExpanded","text":"
    NodeExpanded(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.NodeExpanded.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was expanded.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeHighlighted","title":"NodeHighlighted","text":"
    NodeHighlighted(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.NodeHighlighted.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was highlighted.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeSelected","title":"NodeSelected","text":"
    NodeSelected(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.NodeSelected.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeSelected.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was selected.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Move the cursor down one node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_next_sibling","title":"action_cursor_next_sibling","text":"
    action_cursor_next_sibling()\n

    Move the cursor to the next sibling, or to the paren't sibling if there are no more siblings.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_parent","title":"action_cursor_parent","text":"
    action_cursor_parent()\n

    Move the cursor to the parent node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_parent_next_sibling","title":"action_cursor_parent_next_sibling","text":"
    action_cursor_parent_next_sibling()\n

    Move the cursor to the parent's next sibling.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_previous_sibling","title":"action_cursor_previous_sibling","text":"
    action_cursor_previous_sibling()\n

    Move the cursor to previous sibling, or to the parent if there are no more siblings.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Move the cursor up one node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the cursor down a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the cursor up a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_scroll_end","title":"action_scroll_end","text":"
    action_scroll_end()\n

    Move the cursor to the bottom of the tree.

    Note

    Here bottom means vertically, not branch depth.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_scroll_home","title":"action_scroll_home","text":"
    action_scroll_home()\n

    Move the cursor to the top of the tree.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_select_cursor","title":"action_select_cursor","text":"
    action_select_cursor()\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.action_toggle_expand_all","title":"action_toggle_expand_all","text":"
    action_toggle_expand_all()\n

    Expand or collapse all siblings.

    If all the siblings are collapsed then they will be expanded. Otherwise they will all be collapsed.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_toggle_node","title":"action_toggle_node","text":"
    action_toggle_node()\n

    Toggle the expanded state of the target node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.clear","title":"clear","text":"
    clear()\n

    Clear all nodes under root.

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets.Tree.get_label_width","title":"get_label_width","text":"
    get_label_width(node)\n

    Get the width of the nodes label.

    The default behavior is to call render_label and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    A node.

    required

    Returns:

    Type Description int

    Width in cells.

    "},{"location":"widgets/tree/#textual.widgets.Tree.get_label_width(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.get_node_at_line","title":"get_node_at_line","text":"
    get_node_at_line(line_no)\n

    Get the node for a given line.

    Parameters:

    Name Type Description Default int

    A line number.

    required

    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.get_node_at_line(line_no)","title":"line_no","text":""},{"location":"widgets/tree/#textual.widgets.Tree.get_node_by_id","title":"get_node_by_id","text":"
    get_node_by_id(node_id)\n

    Get a tree node by its ID.

    Parameters:

    Name Type Description Default NodeID

    The ID of the node to get.

    required

    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.get_node_by_id(node_id)","title":"node_id","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor","title":"move_cursor","text":"
    move_cursor(node, animate=False)\n

    Move the cursor to the given node, or reset cursor.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType] | None

    A tree node, or None to reset cursor.

    required bool

    Enable animation

    False"},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line","title":"move_cursor_to_line","text":"
    move_cursor_to_line(line, animate=False)\n

    Move the cursor to the given line.

    Parameters:

    Name Type Description Default int

    The line number (negative indexes are offsets from the last line).

    required

    Enable scrolling animation.

    False

    Raises:

    Type Description IndexError

    If the line doesn't exist.

    "},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line(line)","title":"line","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.process_label","title":"process_label","text":"
    process_label(label)\n

    Process a str or Text value into a label.

    May be overridden in a subclass to change how labels are rendered.

    Parameters:

    Name Type Description Default TextType

    Label.

    required

    Returns:

    Type Description Text

    A Rich Text object.

    "},{"location":"widgets/tree/#textual.widgets.Tree.process_label(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label","title":"render_label","text":"
    render_label(node, base_style, style)\n

    Render a label for the given node. Override this to modify how labels are rendered.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    A tree node.

    required Style

    The base style of the widget.

    required Style

    The additional style for the label.

    required

    Returns:

    Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/tree/#textual.widgets.Tree.render_label(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label(base_style)","title":"base_style","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label(style)","title":"style","text":""},{"location":"widgets/tree/#textual.widgets.Tree.reset","title":"reset","text":"
    reset(label, data=None)\n

    Clear the tree and reset the root node.

    Parameters:

    Name Type Description Default TextType

    The label for the root node.

    required TreeDataType | None

    Optional data for the root node.

    None

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets.Tree.reset(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree.reset(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line","title":"scroll_to_line","text":"
    scroll_to_line(line, animate=True)\n

    Scroll to the given line.

    Parameters:

    Name Type Description Default int

    A line number.

    required bool

    Enable animation.

    True"},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line(line)","title":"line","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node","title":"scroll_to_node","text":"
    scroll_to_node(node, animate=True)\n

    Scroll to the given node.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    Node to scroll in to view.

    required bool

    Animate scrolling.

    True"},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.select_node","title":"select_node","text":"
    select_node(node)\n

    Move the cursor to the given node and select it, or reset cursor.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType] | None

    A tree node to move the cursor to and select, or None to reset cursor.

    required"},{"location":"widgets/tree/#textual.widgets.Tree.select_node(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.unselect","title":"unselect","text":"
    unselect()\n

    Hide and reset the cursor.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_cursor_line","title":"validate_cursor_line","text":"
    validate_cursor_line(value)\n

    Prevent cursor line from going outside of range.

    Parameters:

    Name Type Description Default int

    The value to test.

    required Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_cursor_line(value)","title":"value","text":""},{"location":"widgets/tree/#textual.widgets.Tree.validate_guide_depth","title":"validate_guide_depth","text":"
    validate_guide_depth(value)\n

    Restrict guide depth to reasonable range.

    Parameters:

    Name Type Description Default int

    The value to test.

    required Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_guide_depth(value)","title":"value","text":""},{"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.AddNodeError","title":"AddNodeError","text":"

    Bases: Exception

    Exception raised when there is an error with a request to add a node.

    "},{"location":"widgets/tree/#textual.widgets.tree.RemoveRootError","title":"RemoveRootError","text":"

    Bases: Exception

    Exception raised when trying to remove the root of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"TreeNode","text":"
    TreeNode(\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:

    Name Type Description Default Tree[TreeDataType]

    The tree that the node is being attached to.

    required TreeNode[TreeDataType] | None

    The parent node that this node is being attached to.

    required NodeID

    The ID of the node.

    required Text

    The label for the node.

    required TreeDataType | None

    Optional data to associate with the node.

    None bool

    Should the node be attached in an expanded state?

    True bool

    Should the node allow being expanded by the user?

    True"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(tree)","title":"tree","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(parent)","title":"parent","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(id)","title":"id","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(expanded)","title":"expanded","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(allow_expand)","title":"allow_expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.allow_expand","title":"allow_expand property writable","text":"
    allow_expand\n

    Is this node allowed to expand?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.children","title":"children property","text":"
    children\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\n

    The ID of the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_collapsed","title":"is_collapsed property","text":"
    is_collapsed\n

    Is the node collapsed?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_expanded","title":"is_expanded property","text":"
    is_expanded\n

    Is the node expanded?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_last","title":"is_last property","text":"
    is_last\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\n

    Is this node the root of the tree?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.label","title":"label property writable","text":"
    label\n

    The label for the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.line","title":"line property","text":"
    line\n

    The line number for this node, or -1 if it is not displayed.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.next_sibling","title":"next_sibling property","text":"
    next_sibling\n

    The next sibling below the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.parent","title":"parent property","text":"
    parent\n

    The parent of the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.previous_sibling","title":"previous_sibling property","text":"
    previous_sibling\n

    The previous sibling below the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.siblings","title":"siblings property","text":"
    siblings\n

    The siblings of this node (includes self).

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.tree","title":"tree property","text":"
    tree\n

    The tree that this node is attached to.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add","title":"add","text":"
    add(\n    label,\n    data=None,\n    *,\n    before=None,\n    after=None,\n    expand=False,\n    allow_expand=True\n)\n

    Add a node to the sub-tree.

    Parameters:

    Name Type Description Default TextType

    The new node's label.

    required TreeDataType | None

    Data associated with the new node.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node before.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node after.

    None bool

    Node should be expanded.

    False bool

    Allow use to expand the node via keyboard or mouse.

    True

    Returns:

    Type Description TreeNode[TreeDataType]

    A new Tree node

    Raises:

    Type Description AddNodeError

    If there is a problem with the addition request.

    Note

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

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(before)","title":"before","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(after)","title":"after","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(expand)","title":"expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(allow_expand)","title":"allow_expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf","title":"add_leaf","text":"
    add_leaf(label, data=None, *, before=None, after=None)\n

    Add a 'leaf' node (a node that can not expand).

    Parameters:

    Name Type Description Default TextType

    Label for the node.

    required TreeDataType | None

    Optional data.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node before.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node after.

    None

    Returns:

    Type Description TreeNode[TreeDataType]

    New node.

    Raises:

    Type Description AddNodeError

    If there is a problem with the addition request.

    Note

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

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(before)","title":"before","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(after)","title":"after","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.collapse","title":"collapse","text":"
    collapse()\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","text":"
    collapse_all()\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","text":"
    expand()\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","text":"
    expand_all()\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","text":"
    refresh()\n

    Initiate a refresh (repaint) of this node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.remove","title":"remove","text":"
    remove()\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","text":"
    remove_children()\n

    Remove any child nodes of this node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.set_label","title":"set_label","text":"
    set_label(label)\n

    Set a new label for the node.

    Parameters:

    Name Type Description Default TextType

    A str or Text object with the new label.

    required"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.set_label(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.toggle","title":"toggle","text":"
    toggle()\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","text":"
    toggle_all()\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","text":"

    Bases: Exception

    Exception raised when referring to an unknown TreeNode ID.

    "},{"location":"blog/archive/2024/","title":"2024","text":""},{"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/devlog/page/3/","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.

    UI \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\u258e\u258a\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\u258e \u258afeeds\u258e\u258a\u2590X\u258c\u00a0Case\u00a0sensitive\u258e\u258a\u2590X\u258c\u00a0Regex\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\u258e\u258a\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503162.71.236.120\u00a0-\u00a0-\u00a0[29/Jan/2024:13:34:58\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Net\u2503 \u250352.70.240.171\u00a0-\u00a0-\u00a0[29/Jan/2024:13:35:33\u00a0+0000]\"GET\u00a0/2007/07/10/postmarkup-105/\u00a0HTTP/1.1\"3010\u2503 \u2503121.137.55.45\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:19\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u250398.207.26.211\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:37\u00a0+0000]\"GET\u00a0/feeds/posts\u00a0HTTP/1.1\"3070\"-\"\"Mozilla/5.\u2503 \u250398.207.26.211\u00a0-\u00a0-\u00a0[29/Jan/2024:13:36:42\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"20098063\"-\"\"Mozil\u2503 \u250318.183.222.19\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:44\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u250366.249.64.164\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:46\u00a0+0000]\"GET\u00a0/blog/tech/post/a-texture-mapped-spinning-3d\u2503 \u2503116.203.207.165\u00a0-\u00a0-\u00a0[29/Jan/2024:13:37:55\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"2001182\u2503 \u2503128.65.195.158\u00a0-\u00a0-\u00a0[29/Jan/2024:13:38:44\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"https:/\u2503 \u2503128.65.195.158\u00a0-\u00a0-\u00a0[29/Jan/2024:13:38:46\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"https:/\u2503 \u250351.222.253.12\u00a0-\u00a0-\u00a0[29/Jan/2024:13:41:17\u00a0+0000]\"GET\u00a0/blog/tech/post/css-in-the-terminal-with-pyt\u2503 \u2503154.159.237.77\u00a0-\u00a0-\u00a0[29/Jan/2024:13:42:28\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Moz\u2503 \u250392.247.181.10\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:23\u00a0+0000]\"GET\u00a0/feed/\u00a0HTTP/1.1\"200107059\"https://www.wil\u2503 \u2503134.209.40.52\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:41\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"200118238\u2503 \u2503192.3.134.205\u00a0-\u00a0-\u00a0[29/Jan/2024:13:43:55\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Mozi\u2503 \u2503174.136.108.22\u00a0-\u00a0-\u00a0[29/Jan/2024:13:44:42\u00a0+0000]\"GET\u00a0/feeds/posts/\u00a0HTTP/1.1\"200107059\"-\"\"Tin\u2503 \u250364.71.157.117\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:16\u00a0+0000]\"GET\u00a0/feed/\u00a0HTTP/1.1\"200107059\"-\"\"Feedbin\u00a0fee\u2503 \u2503121.137.55.45\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:19\u00a0+0000]\"GET\u00a0/blog/rootblog/feeds/posts/\u00a0HTTP/1.1\"20010\u2503 \u2503216.244.66.233\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:22\u00a0+0000]\"GET\u00a0/robots.txt\u00a0HTTP/1.1\"200132\"-\"\"Mozilla/\u2503 \u250378.82.5.250\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:29\u00a0+0000]\"GET\u00a0/blog/tech/post/real-working-hyperlinks-in-the\u2503 \u250378.82.5.250\u00a0-\u00a0-\u00a0[29/Jan/2024:13:45:30\u00a0+0000]\"GET\u00a0/favicon.ico\u00a0HTTP/1.1\"2005694\"https://www.w\u2581\u2581\u2503 \u250346.244.252.112\u00a0-\u00a0-\u00a0[29/Jan/2024:13:46:44\u00a0+0000]\"GET\u00a0/blog/tech/feeds/posts/\u00a0HTTP/1.1\"20011823\u2581\u2581\u2503 \u2503\u258c\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\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b f1\u00a0Help^t\u00a0Tail^l\u00a0Line\u00a0nos.^g\u00a0Go\u00a0to\u2193\u00a0Next\u2191\u00a0PreviousTAIL29/01/2024\u00a013:34:58\u00a0\u2022\u00a02540 Frogmouth https://raw.githubusercontent.com/textualize/frogmouth/main/README.md \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\u2513 \u2503\ud83d\uddbc\u00a0\u00a0Discord\u2503ContentsLocalBookmarksHistory \u2503\u2503\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\u2501\u2501 \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\u2503 \u2503\u258e\u258a\u2503\u25bc\u00a0\u2160\u00a0Frogmouth \u2503\u258eFrogmouth\u258a\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Screenshots \u2503\u258e\u258a\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Compatibility \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\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Installing \u2503Frogmouth\u00a0is\u00a0a\u00a0Markdown\u00a0viewer\u00a0/\u00a0browser\u00a0for\u00a0your\u00a0terminal,\u00a0\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Running \u2503built\u00a0with\u00a0Textual.\u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Features \u2503\u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Follow\u00a0this\u00a0project \u2503Frogmouth\u00a0can\u00a0open\u00a0*.md\u00a0files\u00a0locally\u00a0or\u00a0via\u00a0a\u00a0URL.\u00a0There\u00a0is\u00a0a\u00a0\u2503 \u2503familiar\u00a0browser-like\u00a0navigation\u00a0stack,\u00a0history,\u00a0bookmarks,\u00a0and\u2503 \u2503table\u00a0of\u00a0contents.\u2585\u2585\u2503 \u2503\u2503 \u2503A\u00a0quick\u00a0video\u00a0tour\u00a0of\u00a0Frogmouth.\u2503 \u2503\u2503 \u2503https://user-images.githubusercontent.com/554369/235305502-2699\u2503 \u2503a70e-c9a6-495e-990e-67606d84bbfa.mp4\u2503 \u2503\u2503 \u2503(thanks\u00a0Screen\u00a0Studio)\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\u2503 \u2503\u258e\u258a\u2503 \u2503\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Screenshots\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\u2503 \u2503\u258e\u258a\u2503 \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\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\u2503 \u2503\u258e\u258a\u2503 \u2503\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Compatibility\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2503 \u2503\u258e\u258a\u2503 \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\u2503 \u2503Frogmouth\u00a0runs\u00a0on\u00a0Linux,\u00a0macOS,\u00a0and\u00a0Windows.\u00a0Frogmouth\u00a0requires\u2503 \u2503Python\u00a03.8\u00a0or\u00a0above.\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\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0 TUIApp Memray\u00a0live\u00a0tracking\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tue\u00a0Feb\u00a020\u00a013:53:11\u00a02024 \u00a0(\u2229\uff40-\u00b4)\u2283\u2501\u2606\uff9f.*\uff65\uff61\uff9f\u00a0\u256d\u2500\u00a0Heap\u00a0Usage\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e PID:\u00a077542CMD:\u00a0memray\u00a0run\u00a0--live\u00a0-m\u00a0http.server\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 TID:\u00a00x1Thread\u00a01\u00a0of\u00a01\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 Samples:\u00a06Duration:\u00a06.1\u00a0seconds\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\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\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2588\u2588\u2588\u2502 \u2570\u2500\u2500\u00a01.501MB\u00a0(100%\u00a0of\u00a01.501MB\u00a0max)\u00a0\u2500\u256f \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Location\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Total\u00a0Bytes%\u00a0TotalOwn\u00a0Bytes%\u00a0OwnAllocations \u00a0_run_tracker\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.440MB\u00a095.94%\u00a0\u00a01.111KB0.07%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0440\u00a0memray.comman \u00a0_run_module_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.381MB\u00a091.99%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0388\u00a0<frozen\u00a0runpy \u00a0_find_and_load\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.364MB\u00a090.86%\u00a0960.000B0.06%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0361\u00a0<frozen\u00a0impor \u00a0_load_unlocked\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.360MB\u00a090.62%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0355\u00a0<frozen\u00a0impor\u2584\u2584 \u00a0exec_module\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.355MB\u00a090.28%\u00a0\u00a01.225KB0.08%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0351\u00a0<frozen\u00a0impor \u00a0run_module\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.351MB\u00a090.00%\u00a0\u00a01.273KB0.08%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0354\u00a0<frozen\u00a0runpy \u00a0_run_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.334MB\u00a088.90%\u00a0890.000B0.06%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0341\u00a0<frozen\u00a0runpy \u00a0_call_with_frames_removed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.298MB\u00a086.49%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0283\u00a0<frozen\u00a0impor \u00a0get_code\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.168MB\u00a077.80%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0185\u00a0<frozen\u00a0impor \u00a0<module>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01.095MB\u00a072.96%\u00a0\u00a01.688KB0.11%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a095\u00a0http.server\u00a0\u00a0 \u00a0_find_and_load_unlocked\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a059.031KB\u00a0\u00a03.84%\u00a0\u00a0\u00a01.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040\u00a0<frozen\u00a0impor \u00a0test\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a042.097KB\u00a0\u00a02.74%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a027\u00a0http.server\u00a0\u00a0 \u00a0__init__\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a041.565KB\u00a0\u00a02.70%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a020\u00a0socketserver\u00a0 \u00a0getfqdn\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040.933KB\u00a0\u00a02.66%\u00a0\u00a02.135KB0.14%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a018\u00a0socket\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0server_bind\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a040.933KB\u00a0\u00a02.66%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a018\u00a0http.server\u00a0\u00a0 \u00a0search_function\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a038.798KB\u00a0\u00a02.52%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a016\u00a0encodings\u00a0\u00a0\u00a0\u00a0 \u00a0_handle_fromlist\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a029.723KB\u00a0\u00a01.93%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a033\u00a0<frozen\u00a0impor \u00a0<module>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a024.617KB\u00a0\u00a01.60%\u00a0\u00a01.688KB0.11%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0encodings.idn \u00a0_compile\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a023.629KB\u00a0\u00a01.54%\u00a0\u00a0\u00a00.000B0.00%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a011\u00a0re\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u00a0Q\u00a0\u00a0Quit\u00a0\u00a0<\u00a0\u00a0Previous\u00a0Thread\u00a0\u00a0>\u00a0\u00a0Next\u00a0Thread\u00a0\u00a0T\u00a0\u00a0Sort\u00a0by\u00a0Total\u00a0\u00a0O\u00a0\u00a0Sort\u00a0by\u00a0Own\u00a0\u00a0A\u00a0\u00a0Sort\u00a0by\u00a0Allocations\u00a0\u00a0SPACE\u00a0\u00a0Pause\u00a0

    Harlequin\u256d\u2500\u00a0Data\u00a0Catalog\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u00a0Query\u00a0Editor\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\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u25bc\u00a0f1\u00a0db\u2502\u2502\u00a01\u00a0\u00a0select\u2502 \u2502\u2514\u2500\u00a0\u25bc\u00a0main\u00a0sch\u2502\u2502\u00a02\u00a0\u00a0drivers.surname,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0circuits\u00a0t\u2502\u2502\u00a03\u00a0\u00a0drivers.forename,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0constructor_result\u2502\u2502\u00a04\u00a0\u00a0drivers.nationality,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u251c\u2500\u00a0\u25b6\u00a0constructor_standi\u2502\u2502\u00a05\u00a0\u00a0avg(driver_standings.position)asavg_standing,\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0constructors\u00a0t\u2502\u2502\u00a06\u00a0\u00a0avg(driver_standings.points)asavg_points\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0driver_standings\u00a0t\u2502\u2502\u00a07\u00a0\u00a0fromdriver_standings\u2502 \u2502\u251c\u2500\u00a0\u25bc\u00a0drivers\u00a0t\u2502\u2502\u00a08\u00a0\u00a0joindriversondriver_standings.driverid=drivers.driverid\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0code\u00a0s\u2502\u2502\u00a09\u00a0\u00a0joinracesondriver_standings.raceid=races.raceid\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0dob\u00a0d\u2502\u250210\u00a0\u00a0groupby1,\u00a02,\u00a03\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0driverId\u00a0##\u2502\u250211\u00a0\u00a0orderbyavg_standing\u00a0asc\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\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\u00a0\u00a0\u251c\u2500\u00a0driverRef\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0forename\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0nationality\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0number\u00a0s\u2502\u2502\u2502 \u2502\u2502\u00a0\u00a0\u251c\u2500\u00a0surname\u00a0s\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\u256f \u2502\u2502\u00a0\u00a0\u2514\u2500\u00a0url\u00a0s\u2502\u2590X\u258c\u00a0Limit\u00a0500Run\u00a0Query \u2502\u251c\u2500\u00a0\u25b6\u00a0lap_times\u00a0t\u2502\u256d\u2500\u00a0Query\u00a0Results\u00a0(850\u00a0Records)\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\u256e \u2502\u251c\u2500\u00a0\u25b6\u00a0pit_stops\u00a0t\u2502\u2502\u00a0surname\u00a0s\u00a0forename\u00a0s\u00a0nationality\u00a0s\u00a0avg_standing\u00a0#.#\u00a0av\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0qualifying\u00a0t\u2502\u2502\u00a0Hamilton\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Lewis\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02.66\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a014\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0races\u00a0t\u2502\u2502\u00a0Prost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Alain\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0French\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03.51\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a033\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0results\u00a0t\u2502\u2502\u00a0Stewart\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Jackie\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03.78\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a024\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0seasons\u00a0t\u2502\u2502\u00a0Schumacher\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0German\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04.33\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a046\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0sprint_results\u00a0t\u2502\u2502\u00a0Verstappen\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Max\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dutch\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a012\u2502 \u2502\u251c\u2500\u00a0\u25b6\u00a0status\u00a0t\u2502\u2502\u00a0Fangio\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Juan\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Argentine\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.22\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a016\u2502 \u2502\u2514\u2500\u00a0\u25b6\u00a0tbl1\u00a0t\u2502\u2502\u00a0Pablo\u00a0Montoya\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Juan\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Colombian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.25\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a027\u2502 \u2502\u2502\u2502\u00a0Farina\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Nino\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Italian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.27\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a011\u2502 \u2502\u2502\u2502\u00a0Hulme\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Denny\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0New\u00a0Zealander\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.34\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a014\u2502 \u2502\u2502\u2502\u00a0Fagioli\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Luigi\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Italian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.67\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a09.\u2502 \u2502\u2502\u2502\u00a0Clark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Jim\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0British\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.81\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a017\u2502 \u2502\u2502\u2502\u00a0Vettel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Sebastian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0German\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.84\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a010\u2502 \u2502\u2502\u2502\u00a0Senna\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ayrton\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Brazilian\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05.92\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a031\u2502 \u2502\u258c\u2502\u2502\u258c\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\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u00a0CTRL+Q\u00a0\u00a0Quit\u00a0\u00a0F1\u00a0\u00a0Help\u00a0 Stopwatch tutorialstopwatch.pystopwatch.tcss

    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
    Stopwatch {\n    layout: horizontal;\n    background: $boost;\n    height: 5;\n    min-width: 50;\n    margin: 1;\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
    Pride examplepride.py

    PrideApp

    from textual.app import App, ComposeResult\nfrom textual.widgets import Static\n\n\nclass PrideApp(App):\n    \"\"\"Displays a pride flag.\"\"\"\n\n    COLORS = [\"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"purple\"]\n\n    def compose(self) -> ComposeResult:\n        for color in self.COLORS:\n            stripe = Static()\n            stripe.styles.height = \"1fr\"\n            stripe.styles.background = color\n            yield stripe\n\n\nif __name__ == \"__main__\":\n    PrideApp().run()\n
    Calculator examplecalculator.pycalculator.tcss

    CalculatorApp \u2576\u2500\u256e\u00a0\u2576\u256e\u00a0\u2577\u00a0\u2577\u256d\u2500\u2574\u256d\u2500\u256e\u2576\u2500\u256e \u00a0\u2500\u2524\u00a0\u00a0\u2502\u00a0\u2570\u2500\u2524\u2570\u2500\u256e\u2570\u2500\u2524\u250c\u2500\u2518 \u2576\u2500\u256f.\u2576\u2534\u2574\u00a0\u00a0\u2575\u2576\u2500\u256f\u2576\u2500\u256f\u2570\u2500\u2574 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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

    \"\"\"\nAn implementation of a classic calculator, with a layout inspired by macOS calculator.\n\nWorks like a real calculator. Click the buttons or press the equivalent keys.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom textual import events, on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.css.query import NoMatches\nfrom textual.reactive import var\nfrom textual.widgets import Button, Digits\n\n\nclass CalculatorApp(App):\n    \"\"\"A working 'desktop' calculator.\"\"\"\n\n    CSS_PATH = \"calculator.tcss\"\n\n    numbers = var(\"0\")\n    show_ac = var(True)\n    left = var(Decimal(\"0\"))\n    right = var(Decimal(\"0\"))\n    value = var(\"\")\n    operator = var(\"plus\")\n\n    # Maps button IDs on to the corresponding key name\n    NAME_MAP = {\n        \"asterisk\": \"multiply\",\n        \"slash\": \"divide\",\n        \"underscore\": \"plus-minus\",\n        \"full_stop\": \"point\",\n        \"plus_minus_sign\": \"plus-minus\",\n        \"percent_sign\": \"percent\",\n        \"equals_sign\": \"equals\",\n        \"minus\": \"minus\",\n        \"plus\": \"plus\",\n    }\n\n    def watch_numbers(self, value: str) -> None:\n        \"\"\"Called when numbers is updated.\"\"\"\n        self.query_one(\"#numbers\", Digits).update(value)\n\n    def compute_show_ac(self) -> bool:\n        \"\"\"Compute switch to show AC or C button\"\"\"\n        return self.value in (\"\", \"0\") and self.numbers == \"0\"\n\n    def watch_show_ac(self, show_ac: bool) -> None:\n        \"\"\"Called when show_ac changes.\"\"\"\n        self.query_one(\"#c\").display = not show_ac\n        self.query_one(\"#ac\").display = show_ac\n\n    def compose(self) -> ComposeResult:\n        \"\"\"Add our buttons.\"\"\"\n        with Container(id=\"calculator\"):\n            yield Digits(id=\"numbers\")\n            yield Button(\"AC\", id=\"ac\", variant=\"primary\")\n            yield Button(\"C\", id=\"c\", variant=\"primary\")\n            yield Button(\"+/-\", id=\"plus-minus\", variant=\"primary\")\n            yield Button(\"%\", id=\"percent\", variant=\"primary\")\n            yield Button(\"\u00f7\", id=\"divide\", variant=\"warning\")\n            yield Button(\"7\", id=\"number-7\", classes=\"number\")\n            yield Button(\"8\", id=\"number-8\", classes=\"number\")\n            yield Button(\"9\", id=\"number-9\", classes=\"number\")\n            yield Button(\"\u00d7\", id=\"multiply\", variant=\"warning\")\n            yield Button(\"4\", id=\"number-4\", classes=\"number\")\n            yield Button(\"5\", id=\"number-5\", classes=\"number\")\n            yield Button(\"6\", id=\"number-6\", classes=\"number\")\n            yield Button(\"-\", id=\"minus\", variant=\"warning\")\n            yield Button(\"1\", id=\"number-1\", classes=\"number\")\n            yield Button(\"2\", id=\"number-2\", classes=\"number\")\n            yield Button(\"3\", id=\"number-3\", classes=\"number\")\n            yield Button(\"+\", id=\"plus\", variant=\"warning\")\n            yield Button(\"0\", id=\"number-0\", classes=\"number\")\n            yield Button(\".\", id=\"point\")\n            yield Button(\"=\", id=\"equals\", variant=\"warning\")\n\n    def on_key(self, event: events.Key) -> None:\n        \"\"\"Called when the user presses a key.\"\"\"\n\n        def press(button_id: str) -> None:\n            \"\"\"Press a button, should it exist.\"\"\"\n            try:\n                self.query_one(f\"#{button_id}\", Button).press()\n            except NoMatches:\n                pass\n\n        key = event.key\n        if key.isdecimal():\n            press(f\"number-{key}\")\n        elif key == \"c\":\n            press(\"c\")\n            press(\"ac\")\n        else:\n            button_id = self.NAME_MAP.get(key)\n            if button_id is not None:\n                press(self.NAME_MAP.get(key, key))\n\n    @on(Button.Pressed, \".number\")\n    def number_pressed(self, event: Button.Pressed) -> None:\n        \"\"\"Pressed a number.\"\"\"\n        assert event.button.id is not None\n        number = event.button.id.partition(\"-\")[-1]\n        self.numbers = self.value = self.value.lstrip(\"0\") + number\n\n    @on(Button.Pressed, \"#plus-minus\")\n    def plus_minus_pressed(self) -> None:\n        \"\"\"Pressed + / -\"\"\"\n        self.numbers = self.value = str(Decimal(self.value or \"0\") * -1)\n\n    @on(Button.Pressed, \"#percent\")\n    def percent_pressed(self) -> None:\n        \"\"\"Pressed %\"\"\"\n        self.numbers = self.value = str(Decimal(self.value or \"0\") / Decimal(100))\n\n    @on(Button.Pressed, \"#point\")\n    def pressed_point(self) -> None:\n        \"\"\"Pressed .\"\"\"\n        if \".\" not in self.value:\n            self.numbers = self.value = (self.value or \"0\") + \".\"\n\n    @on(Button.Pressed, \"#ac\")\n    def pressed_ac(self) -> None:\n        \"\"\"Pressed AC\"\"\"\n        self.value = \"\"\n        self.left = self.right = Decimal(0)\n        self.operator = \"plus\"\n        self.numbers = \"0\"\n\n    @on(Button.Pressed, \"#c\")\n    def pressed_c(self) -> None:\n        \"\"\"Pressed C\"\"\"\n        self.value = \"\"\n        self.numbers = \"0\"\n\n    def _do_math(self) -> None:\n        \"\"\"Does the math: LEFT OPERATOR RIGHT\"\"\"\n        try:\n            if self.operator == \"plus\":\n                self.left += self.right\n            elif self.operator == \"minus\":\n                self.left -= self.right\n            elif self.operator == \"divide\":\n                self.left /= self.right\n            elif self.operator == \"multiply\":\n                self.left *= self.right\n            self.numbers = str(self.left)\n            self.value = \"\"\n        except Exception:\n            self.numbers = \"Error\"\n\n    @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\n    @on(Button.Pressed, \"#equals\")\n    def pressed_equals(self) -> None:\n        \"\"\"Pressed =\"\"\"\n        if self.value:\n            self.right = Decimal(self.value)\n        self._do_math()\n\n\nif __name__ == \"__main__\":\n    CalculatorApp().run(inline=True)\n
    Screen {\n    overflow: auto;\n}\n\n#calculator {\n    layout: grid;\n    grid-size: 4;\n    grid-gutter: 1 2;\n    grid-columns: 1fr;\n    grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr;\n    margin: 1 2;\n    min-height: 25;\n    min-width: 26;\n    height: 100%;\n\n    &:inline {\n        margin: 0 2;\n    }\n}\n\nButton {\n    width: 100%;\n    height: 100%;\n}\n\n#numbers {\n    column-span: 4;\n    padding: 0 1;\n    height: 100%;\n    background: $primary-lighten-2;\n    color: $text;\n    content-align: center middle;\n    text-align: right;\n}\n\n#number-0 {\n    column-span: 2;\n}\n
    "},{"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 comprehensive 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, particularly 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.

    Changed in version 0.80.0

    Textual added an ansi_color boolean to App. If you set this to True, then Textual will not attempt to convert ANSI colors. Note that you will lose transparency effects if you enable this setting.

    "},{"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 \u00a0^b\u00a0Sidebar\u00a0\u00a0^t\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0^s\u00a0Screenshot\u00a0\u00a0f1\u00a0Notes\u00a0\u00a0^q\u00a0Quit\u00a0\u258f^p\u00a0palette

    "},{"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/#widget-examples","title":"Widget examples","text":"

    In addition to the example apps, you can also find the code listings used to generate the screenshots in these docs in the docs/examples directory.

    "},{"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.17 \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.11 \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.11 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0a\u00a0Add\u00a0\u00a0r\u00a0Remove\u00a0\u258f^p\u00a0palette

    Info

    Did you notice the ^p palette at the bottom right hand corner? This is the Command Palette. You can think of it as a dedicated command prompt for your app.

    "},{"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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

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

    stopwatch01.py \u2b58StopwatchApp \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    "},{"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.05Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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.05Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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.05Reset \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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.11 \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.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 \u00a0d\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u258f^p\u00a0palette

    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.10 \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\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0a\u00a0Add\u00a0\u00a0r\u00a0Remove\u00a0\u258f^p\u00a0palette

    "},{"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 Lady\u00a0Jessica 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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    "},{"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\u2576\u2500\u256e\u00a0\u2576\u256e\u00a0\u2577\u00a0\u2577\u2576\u256e\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u256e\u2576\u2500\u256e\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u2576\u2500\u256e\u00a0\u256d\u2500\u2574\u256d\u2500\u256e\u256d\u2500\u256e\u2576\u2500\u2510\u2551 \u2551\u00a0\u2500\u2524\u00a0\u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0\u2502\u00a0\u00a0\u2570\u2500\u256e\u2570\u2500\u2524\u250c\u2500\u2518\u00a0\u251c\u2500\u256e\u2570\u2500\u256e\u00a0\u2500\u2524\u00a0\u2570\u2500\u256e\u251c\u2500\u2524\u2570\u2500\u2524\u00a0\u00a0\u2502\u2551 \u2551\u2576\u2500\u256f.\u2576\u2534\u2574\u00a0\u00a0\u2575\u2576\u2534\u2574,\u2576\u2500\u256f\u2576\u2500\u256f\u2570\u2500\u2574,\u2570\u2500\u256f\u2576\u2500\u256f\u2576\u2500\u256f,\u2576\u2500\u256f\u2570\u2500\u256f\u2576\u2500\u256f\u00a0\u00a0\u2575\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.ruff_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__\u2585\u2585 \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

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

    A footer to display and interact with key bindings.

    Footer reference

    FooterApp \u00a0q\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0del\u00a0Delete\u00a0the\u00a0thing\u00a0\u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    "},{"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 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258aMarkdown\u00a0Viewer \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a \u258aFeatures \u258a \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 \u258aTables \u258a \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a \u258aName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Type\u00a0Default\u00a0Description\u00a0\u00a0\u00a0\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u00a0 \u258ashow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258azebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u00a0\u00a0\u00a0\u00a0 \u258aheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258ashow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u258a \u258a \u258aCode\u00a0Blocks \u258a\u2585\u2585 \u258aCode\u00a0blocks\u00a0are\u00a0syntax\u00a0highlighted,\u00a0with\u00a0guidelines. \u258a \u258a \u258aclassListViewExample(App): \u258a\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u258a\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldListView(

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

    Display a markdown document.

    Markdown reference

    MarkdownExampleApp Markdown\u00a0Document This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. Features 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/#maskedinput","title":"MaskedInput","text":"

    A control to enter input according to a template mask.

    MaskedInput reference

    MaskedInputApp Enter\u00a0a\u00a0valid\u00a0credit\u00a0card\u00a0number. \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 \u258a0000-0000-0000-0000\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

    "},{"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 \u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    "},{"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\u00a0Add\u00a0tab\u00a0\u00a0r\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0c\u00a0Clear\u00a0tabs\u00a0\u258f^p\u00a0palette

    "},{"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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    "},{"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 \u258a1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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":"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 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":"textual.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 = (\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.CommandCallback","title":"CommandCallback module-attribute","text":"
    CommandCallback = (\n    \"Callable[[], Awaitable[Any]] | Callable[[], Any]\"\n)\n

    Signature for callbacks used in get_system_commands

    "},{"location":"api/app/#textual.app.ScreenType","title":"ScreenType module-attribute","text":"
    ScreenType = TypeVar('ScreenType', bound=Screen)\n

    Type var for a Screen, used in get_screen.

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

    Bases: Exception

    Base class for exceptions relating to actions.

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

    Bases: ModeError

    Raised when attempting to remove the currently active mode.

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

    Bases: Generic[ReturnType], DOMNode

    The base class for Textual Applications.

    Parameters:

    Name Type Description Default Type[Driver] | None

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

    None CSSPathType | 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.

    None bool

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

    False bool

    Allow ANSI colors if True, or convert ANSI colors to to RGB if False.

    False

    Raises:

    Type Description CssPathError

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

    "},{"location":"api/app/#textual.app.App(driver_class)","title":"driver_class","text":""},{"location":"api/app/#textual.app.App(css_path)","title":"css_path","text":""},{"location":"api/app/#textual.app.App(watch_css)","title":"watch_css","text":""},{"location":"api/app/#textual.app.App(ansi_color)","title":"ansi_color","text":""},{"location":"api/app/#textual.app.App.ALLOW_IN_MAXIMIZED_VIEW","title":"ALLOW_IN_MAXIMIZED_VIEW class-attribute","text":"
    ALLOW_IN_MAXIMIZED_VIEW = 'Footer'\n

    The default value of Screen.ALLOW_IN_MAXIMIZED_VIEW.

    "},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS = '*'\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.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True\n    )\n]\n

    The default key bindings.

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

    Set to text to show in the key panel.

    "},{"location":"api/app/#textual.app.App.CLOSE_TIMEOUT","title":"CLOSE_TIMEOUT class-attribute instance-attribute","text":"
    CLOSE_TIMEOUT = 5.0\n

    Timeout waiting for widget's to close, or None for no timeout.

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

    Command providers used by the command palette.

    Should be a set of command.Provider classes.

    "},{"location":"api/app/#textual.app.App.COMMAND_PALETTE_BINDING","title":"COMMAND_PALETTE_BINDING class-attribute","text":"
    COMMAND_PALETTE_BINDING = 'ctrl+p'\n

    The key that launches the command palette (if enabled by App.ENABLE_COMMAND_PALETTE).

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

    How the command palette key should be displayed in the footer (or None for default).

    "},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
    CSS = ''\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 = 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 = True\n

    Should the command palette be enabled for the application?

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

    Use escape key to minimize widgets (potentially overriding bindings).

    This is the default value, used if the active screen's ESCAPE_TO_MINIMIZE is not changed from None.

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

    Number of blank lines above an inline app.

    "},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
    MODES = {}\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.NOTIFICATION_TIMEOUT","title":"NOTIFICATION_TIMEOUT class-attribute","text":"
    NOTIFICATION_TIMEOUT = 5\n

    Default number of seconds to show notifications before removing them.

    "},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
    SCREENS = {}\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 = 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 = 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.TOOLTIP_DELAY","title":"TOOLTIP_DELAY class-attribute instance-attribute","text":"
    TOOLTIP_DELAY = 0.5\n

    The time in seconds after which a tooltip gets displayed.

    "},{"location":"api/app/#textual.app.App.active_bindings","title":"active_bindings property","text":"
    active_bindings\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, ActiveBinding]

    A dict that maps keys on to binding information.

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

    Determines what type of animations the app will display.

    See textual.constants.TEXTUAL_ANIMATIONS.

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

    The animator object.

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

    Allow ANSI colors in UI?

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

    The ANSI TerminalTheme currently being used.

    Defines how colors defined as ANSI (e.g. magenta) inside Rich renderables are mapped to hex codes.

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

    Maps ANSI colors to hex colors using a Rich TerminalTheme object while in dark mode.

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

    Maps ANSI colors to hex colors using a Rich TerminalTheme object while in light mode.

    "},{"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\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\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(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\n

    Is debug mode enabled?

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

    Use the escape key to minimize?

    When a widget is maximized, this boolean determines if the escape key will minimize the widget (potentially overriding any bindings).

    The default logic is to use the screen's ESCAPE_TO_MINIMIZE classvar if it is set to True or False. If the classvar on the screen is not set (and left as None), then the app's ESCAPE_TO_MINIMIZE is used.

    "},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
    focused\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_attached","title":"is_attached property","text":"
    is_attached\n

    Is this node linked to the app through the DOM?

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

    Is this a root node (i.e. the App)?

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

    Is the app running in 'headless' mode?

    Headless mode is used when running tests with run_test.

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

    Is the app running in 'inline' mode?

    "},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
    log\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.return_code","title":"return_code property","text":"
    return_code\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 hasn'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\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\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\n

    A snapshot of the current screen stack.

    Returns:

    Type Description list[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 = 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 = 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\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 = SUB_TITLE if SUB_TITLE is not None else ''\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 = TITLE if TITLE is not None else f'{__name__}'\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 = 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\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":"
    action_add_class(selector, class_name)\n

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

    Parameters:

    Name Type Description Default str

    Selects the widget to add the class to.

    required str

    The class to add to the selected widget.

    required"},{"location":"api/app/#textual.app.App.action_add_class(selector)","title":"selector","text":""},{"location":"api/app/#textual.app.App.action_add_class(class_name)","title":"class_name","text":""},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
    action_back()\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":"
    action_bell()\n

    An action to play the terminal 'bell'.

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

    Show the Textual command palette.

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

    An action to focus the given widget.

    Parameters:

    Name Type Description Default str

    ID of widget to focus.

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

    An action to focus the next widget.

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

    An action to focus the previous widget.

    "},{"location":"api/app/#textual.app.App.action_hide_help_panel","title":"action_hide_help_panel","text":"
    action_hide_help_panel()\n

    Hide the keys panel (if present).

    "},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
    action_pop_screen()\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":"
    action_push_screen(screen)\n

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

    Parameters:

    Name Type Description Default str

    Name of the screen.

    required"},{"location":"api/app/#textual.app.App.action_push_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
    action_quit()\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":"
    action_remove_class(selector, class_name)\n

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

    Parameters:

    Name Type Description Default str

    Selects the widget to remove the class from.

    required str

    The class to remove from the selected widget.

    required"},{"location":"api/app/#textual.app.App.action_remove_class(selector)","title":"selector","text":""},{"location":"api/app/#textual.app.App.action_remove_class(class_name)","title":"class_name","text":""},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot","text":"
    action_screenshot(filename=None, path=None)\n

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

    Parameters:

    Name Type Description Default str | None

    Filename of screenshot, or None to auto-generate.

    None str | None

    Path to directory. Defaults to the user's Downloads directory.

    None"},{"location":"api/app/#textual.app.App.action_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.action_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.action_show_help_panel","title":"action_show_help_panel","text":"
    action_show_help_panel()\n

    Show the keys panel.

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

    An action to simulate a key press.

    This will invoke the same actions as if the user had pressed the key.

    Parameters:

    Name Type Description Default str

    The key to process.

    required"},{"location":"api/app/#textual.app.App.action_simulate_key(key)","title":"key","text":""},{"location":"api/app/#textual.app.App.action_suspend_process","title":"action_suspend_process","text":"
    action_suspend_process()\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":"
    action_switch_mode(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":"
    action_switch_screen(screen)\n

    An action to switch screens.

    Parameters:

    Name Type Description Default str

    Name of the screen.

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

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

    Parameters:

    Name Type Description Default str

    Selects the widget to toggle the class on.

    required str

    The class to toggle on the selected widget.

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

    An action to toggle dark mode.

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

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

    Parameters:

    Name Type Description Default str

    The new mode.

    required str | Callable[[], Screen]

    The base screen associated with the given mode.

    required

    Raises:

    Type Description InvalidModeError

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

    "},{"location":"api/app/#textual.app.App.add_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.add_mode(base_screen)","title":"base_screen","text":""},{"location":"api/app/#textual.app.App.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    See the guide for how to use the animation system.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required float | Animatable

    The value to animate to.

    required object

    The final value of the animation.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/app/#textual.app.App.animate(attribute)","title":"attribute","text":""},{"location":"api/app/#textual.app.App.animate(value)","title":"value","text":""},{"location":"api/app/#textual.app.App.animate(final_value)","title":"final_value","text":""},{"location":"api/app/#textual.app.App.animate(duration)","title":"duration","text":""},{"location":"api/app/#textual.app.App.animate(speed)","title":"speed","text":""},{"location":"api/app/#textual.app.App.animate(delay)","title":"delay","text":""},{"location":"api/app/#textual.app.App.animate(easing)","title":"easing","text":""},{"location":"api/app/#textual.app.App.animate(on_complete)","title":"on_complete","text":""},{"location":"api/app/#textual.app.App.animate(level)","title":"level","text":""},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update","text":"
    batch_update()\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","text":"
    begin_capture_print(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:

    Name Type Description Default MessageTarget

    The widget where print content will be sent.

    required bool

    Capture stdout.

    True bool

    Capture stderr.

    True"},{"location":"api/app/#textual.app.App.begin_capture_print(target)","title":"target","text":""},{"location":"api/app/#textual.app.App.begin_capture_print(stdout)","title":"stdout","text":""},{"location":"api/app/#textual.app.App.begin_capture_print(stderr)","title":"stderr","text":""},{"location":"api/app/#textual.app.App.bell","title":"bell","text":"
    bell()\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","text":"
    bind(\n    keys,\n    action,\n    *,\n    description=\"\",\n    show=True,\n    key_display=None\n)\n

    Bind a key to an action.

    Parameters:

    Name Type Description Default str

    A comma separated list of keys, i.e.

    required str

    Action to bind to.

    required str

    Short description of action.

    '' bool

    Show key in UI.

    True str | None

    Replacement text for key, or None to use default.

    None"},{"location":"api/app/#textual.app.App.bind(keys)","title":"keys","text":""},{"location":"api/app/#textual.app.App.bind(action)","title":"action","text":""},{"location":"api/app/#textual.app.App.bind(description)","title":"description","text":""},{"location":"api/app/#textual.app.App.bind(show)","title":"show","text":""},{"location":"api/app/#textual.app.App.bind(key_display)","title":"key_display","text":""},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread","text":"
    call_from_thread(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:

    Name Type Description Default Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]]

    A callable to run.

    required Any

    Arguments to the callback.

    () 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.call_from_thread(callback)","title":"callback","text":""},{"location":"api/app/#textual.app.App.call_from_thread(*args)","title":"*args","text":""},{"location":"api/app/#textual.app.App.call_from_thread(**kwargs)","title":"**kwargs","text":""},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse","text":"
    capture_mouse(widget)\n

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

    Parameters:

    Name Type Description Default Widget | None

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

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

    Clear all the current notifications.

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

    Yield child widgets for a container.

    This method should be implemented in a subclass.

    "},{"location":"api/app/#textual.app.App.copy_to_clipboard","title":"copy_to_clipboard","text":"
    copy_to_clipboard(text)\n

    Copy text to the clipboard.

    Note

    This does not work on macOS Terminal, but will work on most other terminals.

    Parameters:

    Name Type Description Default str

    Text you wish to copy to the clipboard.

    required"},{"location":"api/app/#textual.app.App.copy_to_clipboard(text)","title":"text","text":""},{"location":"api/app/#textual.app.App.deliver_binary","title":"deliver_binary","text":"
    deliver_binary(\n    path_or_file,\n    *,\n    save_directory=None,\n    save_filename=None,\n    open_method=\"download\",\n    mime_type=None,\n    name=None\n)\n

    Deliver a binary file to the end-user of the application.

    If an IO object is supplied, it will be closed by this method and must not be used after it is supplied to this method.

    If running in a terminal, this will save the file to the user's downloads directory.

    If running via a web browser, this will initiate a download via a single-use URL.

    This operation runs in a thread when running on web, so this method returning does not indicate that the file has been delivered.

    After the file has been delivered, a DeliveryComplete message will be posted to this App, which contains the delivery_key returned by this method. By handling this message, you can add custom logic to your application that fires only after the file has been delivered.

    Parameters:

    Name Type Description Default str | Path | BinaryIO

    The path or file-like object to save.

    required str | Path | None

    The directory to save the file to. If None, the default \"downloads\" directory will be used. This argument is ignored when running via the web.

    None str | None

    The filename to save the file to. If None, the following logic applies to generate the filename: - If path_or_file is a file-like object, the filename will be taken from the name attribute if available. - If path_or_file is a path, the filename will be taken from the path. - If a filename is not available, a filename will be generated using the App's title and the current date and time.

    None Literal['browser', 'download']

    The method to use to open the file. \"browser\" will open the file in the web browser, \"download\" will initiate a download. Note that this can sometimes be impacted by the browser's settings.

    'download' str | None

    The MIME type of the file or None to guess based on file extension. If no MIME type is supplied and we cannot guess the MIME type, from the file extension, the MIME type will be set to \"application/octet-stream\".

    None str | None

    A user-defined named which will be returned in DeliveryComplete and DeliveryComplete.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_binary(path_or_file)","title":"path_or_file","text":""},{"location":"api/app/#textual.app.App.deliver_binary(save_directory)","title":"save_directory","text":""},{"location":"api/app/#textual.app.App.deliver_binary(save_filename)","title":"save_filename","text":""},{"location":"api/app/#textual.app.App.deliver_binary(open_method)","title":"open_method","text":""},{"location":"api/app/#textual.app.App.deliver_binary(mime_type)","title":"mime_type","text":""},{"location":"api/app/#textual.app.App.deliver_binary(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot","title":"deliver_screenshot","text":"
    deliver_screenshot(\n    filename=None, path=None, time_format=None\n)\n

    Deliver a screenshot of the app.

    This with save the screenshot when running locally, or serve it when the app is running in a web browser.

    Parameters:

    Name Type Description Default str | None

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

    None str | None

    Path to directory for output when saving locally (not used when app is running in the browser). Defaults to current working directory.

    None str | 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.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.deliver_screenshot(time_format)","title":"time_format","text":""},{"location":"api/app/#textual.app.App.deliver_text","title":"deliver_text","text":"
    deliver_text(\n    path_or_file,\n    *,\n    save_directory=None,\n    save_filename=None,\n    open_method=\"download\",\n    encoding=None,\n    mime_type=None,\n    name=None\n)\n

    Deliver a text file to the end-user of the application.

    If a TextIO object is supplied, it will be closed by this method and must not be used after this method is called.

    If running in a terminal, this will save the file to the user's downloads directory.

    If running via a web browser, this will initiate a download via a single-use URL.

    After the file has been delivered, a DeliveryComplete message will be posted to this App, which contains the delivery_key returned by this method. By handling this message, you can add custom logic to your application that fires only after the file has been delivered.

    Parameters:

    Name Type Description Default str | Path | TextIO

    The path or file-like object to save.

    required str | Path | None

    The directory to save the file to.

    None str | None

    The filename to save the file to. If path_or_file is a file-like object, the filename will be generated from the name attribute if available. If path_or_file is a path the filename will be generated from the path.

    None str | None

    The encoding to use when saving the file. If None, the encoding will be determined by supplied file-like object (if possible). If this is not possible, 'utf-8' will be used.

    None str | None

    The MIME type of the file or None to guess based on file extension. If no MIME type is supplied and we cannot guess the MIME type, from the file extension, the MIME type will be set to \"text/plain\".

    None str | None

    A user-defined named which will be returned in DeliveryComplete and DeliveryComplete.

    None

    Returns:

    Type Description str | None

    The delivery key that uniquely identifies the file delivery.

    "},{"location":"api/app/#textual.app.App.deliver_text(path_or_file)","title":"path_or_file","text":""},{"location":"api/app/#textual.app.App.deliver_text(save_directory)","title":"save_directory","text":""},{"location":"api/app/#textual.app.App.deliver_text(save_filename)","title":"save_filename","text":""},{"location":"api/app/#textual.app.App.deliver_text(encoding)","title":"encoding","text":""},{"location":"api/app/#textual.app.App.deliver_text(mime_type)","title":"mime_type","text":""},{"location":"api/app/#textual.app.App.deliver_text(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print","text":"
    end_capture_print(target)\n

    End capturing of prints.

    Parameters:

    Name Type Description Default MessageTarget

    The widget that was capturing prints.

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

    Exit the app, and return the supplied result.

    Parameters:

    Name Type Description Default ReturnType | None

    Return value.

    None int

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

    0 RenderableType | None

    Optional message to display on exit.

    None"},{"location":"api/app/#textual.app.App.exit(result)","title":"result","text":""},{"location":"api/app/#textual.app.App.exit(return_code)","title":"return_code","text":""},{"location":"api/app/#textual.app.App.exit(message)","title":"message","text":""},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot","text":"
    export_screenshot(*, title=None, simplify=False)\n

    Export an SVG screenshot of the current screen.

    See also save_screenshot which writes the screenshot to a file.

    Parameters:

    Name Type Description Default str | None

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

    None bool

    Simplify the segments by combining contiguous segments with the same style.

    False"},{"location":"api/app/#textual.app.App.export_screenshot(title)","title":"title","text":""},{"location":"api/app/#textual.app.App.export_screenshot(simplify)","title":"simplify","text":""},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id","text":"
    get_child_by_id(id: str) -> Widget\n
    get_child_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_child_by_id(id, expect_type=None)\n

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

    Parameters:

    Name Type Description Default str

    The ID of the node to search for.

    required type[ExpectType] | None

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

    None

    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_id(id)","title":"id","text":""},{"location":"api/app/#textual.app.App.get_child_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type","text":"
    get_child_by_type(expect_type)\n

    Get a child of a give type.

    Parameters:

    Name Type Description Default type[ExpectType]

    The type of the expected child.

    required

    Raises:

    Type Description NoMatches

    If no valid child is found.

    Returns:

    Type Description ExpectType

    A widget.

    "},{"location":"api/app/#textual.app.App.get_child_by_type(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables","text":"
    get_css_variables()\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_default_screen","title":"get_default_screen","text":"
    get_default_screen()\n

    Get the default screen.

    This is called when the App is first composed. The returned screen instance will be the first screen on the stack.

    Implement this method if you would like to use a custom Screen as the default screen.

    Returns:

    Type Description Screen

    A screen instance.

    "},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class","text":"
    get_driver_class()\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","text":"
    get_key_display(binding)\n

    Format a bound key for display in footer / key panel etc.

    Note

    You can implement this in a subclass if you want to change how keys are displayed in your app.

    Parameters:

    Name Type Description Default Binding

    A Binding.

    required

    Returns:

    Type Description str

    A string used to represent the key.

    "},{"location":"api/app/#textual.app.App.get_key_display(binding)","title":"binding","text":""},{"location":"api/app/#textual.app.App.get_loading_widget","title":"get_loading_widget","text":"
    get_loading_widget()\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_pseudo_classes","title":"get_pseudo_classes","text":"
    get_pseudo_classes()\n

    Pseudo classes for a widget.

    Returns:

    Type Description Iterable[str]

    Names of the pseudo classes.

    "},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen","text":"
    get_screen(screen: ScreenType) -> ScreenType\n
    get_screen(screen: str) -> Screen\n
    get_screen(\n    screen: str,\n    screen_class: Type[ScreenType] | None = None,\n) -> ScreenType\n
    get_screen(\n    screen: ScreenType,\n    screen_class: Type[ScreenType] | None = None,\n) -> ScreenType\n
    get_screen(screen, screen_class=None)\n

    Get an installed screen.

    Example
    my_screen = self.get_screen(\"settings\", MyScreen)\n

    Parameters:

    Name Type Description Default Screen | str

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

    required Type[Screen] | None

    Class of expected screen, or None for any screen class.

    None

    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_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.get_screen(screen_class)","title":"screen_class","text":""},{"location":"api/app/#textual.app.App.get_system_commands","title":"get_system_commands","text":"
    get_system_commands(screen)\n

    A generator of system commands used in the command palette.

    Parameters:

    Name Type Description Default Screen

    The screen where the command palette was invoked from.

    required

    Implement this method in your App subclass if you want to add custom commands. Here is an example:

    def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:\n    yield from super().get_system_commands(screen)\n    yield SystemCommand(\"Bell\", \"Ring the bell\", self.bell)\n

    Note

    Requires that SystemCommandsProvider is in App.COMMANDS class variable.

    Yields:

    Type Description Iterable[SystemCommand]

    SystemCommand instances.

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

    Get the widget under the given coordinates.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description tuple[Widget, Region]

    The widget and the widget's screen region.

    "},{"location":"api/app/#textual.app.App.get_widget_at(x)","title":"x","text":""},{"location":"api/app/#textual.app.App.get_widget_at(y)","title":"y","text":""},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id","text":"
    get_widget_by_id(id: str) -> Widget\n
    get_widget_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_widget_by_id(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:

    Name Type Description Default str

    The ID to search for in the subtree

    required type[ExpectType] | None

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

    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.get_widget_by_id(id)","title":"id","text":""},{"location":"api/app/#textual.app.App.get_widget_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/app/#textual.app.App.handle_bindings_clash","title":"handle_bindings_clash","text":"
    handle_bindings_clash(clashed_bindings, node)\n

    Handle a clash between bindings.

    Bindings clashes are likely due to users setting conflicting keys via their keymap.

    This method is intended to be overridden by subclasses.

    Textual will call this each time a clash is encountered - which may be on each keypress if a clashing widget is focused or is in the bindings chain.

    Parameters:

    Name Type Description Default set[Binding]

    The bindings that are clashing.

    required DOMNode

    The node that has the clashing bindings.

    required"},{"location":"api/app/#textual.app.App.handle_bindings_clash(clashed_bindings)","title":"clashed_bindings","text":""},{"location":"api/app/#textual.app.App.handle_bindings_clash(node)","title":"node","text":""},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen","text":"
    install_screen(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:

    Name Type Description Default Screen

    Screen to install.

    required str

    Unique name to identify the screen.

    required

    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.install_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.install_screen(name)","title":"name","text":""},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted","text":"
    is_mounted(widget)\n

    Check if a widget is mounted.

    Parameters:

    Name Type Description Default Widget

    A widget.

    required

    Returns:

    Type Description bool

    True of the widget is mounted.

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

    Check if a given screen has been installed.

    Parameters:

    Name Type Description Default Screen | str

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

    required

    Returns:

    Type Description bool

    True if the screen is currently installed,

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

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

    Parameters:

    Name Type Description Default Widget

    The widget(s) to mount.

    () int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(*widgets)","title":"*widgets","text":""},{"location":"api/app/#textual.app.App.mount(before)","title":"before","text":""},{"location":"api/app/#textual.app.App.mount(after)","title":"after","text":""},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all","text":"
    mount_all(widgets, *, before=None, after=None)\n

    Mount widgets from an iterable.

    Parameters:

    Name Type Description Default Iterable[Widget]

    An iterable of widgets.

    required int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(widgets)","title":"widgets","text":""},{"location":"api/app/#textual.app.App.mount_all(before)","title":"before","text":""},{"location":"api/app/#textual.app.App.mount_all(after)","title":"after","text":""},{"location":"api/app/#textual.app.App.notify","title":"notify","text":"
    notify(\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=None\n)\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters:

    Name Type Description Default str

    The message for the notification.

    required str

    The title for the notification.

    '' SeverityLevel

    The severity of the notification.

    'information' float | None

    The timeout (in seconds) for the notification, or None for default.

    None

    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.notify(message)","title":"message","text":""},{"location":"api/app/#textual.app.App.notify(title)","title":"title","text":""},{"location":"api/app/#textual.app.App.notify(severity)","title":"severity","text":""},{"location":"api/app/#textual.app.App.notify(timeout)","title":"timeout","text":""},{"location":"api/app/#textual.app.App.open_url","title":"open_url","text":"
    open_url(url, *, new_tab=True)\n

    Open a URL in the default web browser.

    Parameters:

    Name Type Description Default str

    The URL to open.

    required bool

    Whether to open the URL in a new tab.

    True"},{"location":"api/app/#textual.app.App.open_url(url)","title":"url","text":""},{"location":"api/app/#textual.app.App.open_url(new_tab)","title":"new_tab","text":""},{"location":"api/app/#textual.app.App.panic","title":"panic","text":"
    panic(*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:

    Name Type Description Default RenderableType

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

    ()"},{"location":"api/app/#textual.app.App.panic(*renderables)","title":"*renderables","text":""},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen","text":"
    pop_screen()\n

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

    Returns:

    Type Description AwaitComplete

    The screen that was replaced.

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

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

    "},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen","text":"
    push_screen(\n    screen: Screen[ScreenResultType] | str,\n    callback: (\n        ScreenResultCallbackType[ScreenResultType] | None\n    ) = None,\n    wait_for_dismiss: Literal[False] = False,\n) -> AwaitMount\n
    push_screen(\n    screen: Screen[ScreenResultType] | str,\n    callback: (\n        ScreenResultCallbackType[ScreenResultType] | None\n    ) = None,\n    wait_for_dismiss: Literal[True] = True,\n) -> Future[ScreenResultType]\n
    push_screen(screen, callback=None, wait_for_dismiss=False)\n

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

    Parameters:

    Name Type Description Default Screen[ScreenResultType] | str

    A Screen instance or the name of an installed screen.

    required ScreenResultCallbackType[ScreenResultType] | None

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

    None bool

    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.

    False

    Raises:

    Type Description NoActiveWorker

    If using wait_for_dismiss outside of a worker.

    Returns:

    Type Description AwaitMount | 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(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.push_screen(callback)","title":"callback","text":""},{"location":"api/app/#textual.app.App.push_screen(wait_for_dismiss)","title":"wait_for_dismiss","text":""},{"location":"api/app/#textual.app.App.push_screen_wait","title":"push_screen_wait async","text":"
    push_screen_wait(\n    screen: Screen[ScreenResultType],\n) -> ScreenResultType\n
    push_screen_wait(screen: str) -> Any\n
    push_screen_wait(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:

    Name Type Description Default Screen[ScreenResultType] | str

    A screen or the name of an installed screen.

    required

    Returns:

    Type Description ScreenResultType | Any

    The screen's result.

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

    Recompose the widget.

    Recomposing will remove children and call self.compose again to remount.

    "},{"location":"api/app/#textual.app.App.refresh","title":"refresh","text":"
    refresh(*, repaint=True, layout=False, recompose=False)\n

    Refresh the entire screen.

    Parameters:

    Name Type Description Default bool

    Repaint the widget (will call render() again).

    True bool

    Also layout widgets in the view.

    False bool

    Re-compose the widget (will remove and re-mount children).

    False

    Returns:

    Type Description Self

    The App instance.

    "},{"location":"api/app/#textual.app.App.refresh(repaint)","title":"repaint","text":""},{"location":"api/app/#textual.app.App.refresh(layout)","title":"layout","text":""},{"location":"api/app/#textual.app.App.refresh(recompose)","title":"recompose","text":""},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css","text":"
    refresh_css(animate=True)\n

    Refresh CSS.

    Parameters:

    Name Type Description Default bool

    Also execute CSS animations.

    True"},{"location":"api/app/#textual.app.App.refresh_css(animate)","title":"animate","text":""},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode","text":"
    remove_mode(mode)\n

    Removes a mode from the app.

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

    Parameters:

    Name Type Description Default str

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

    required

    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.remove_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.render","title":"render","text":"
    render()\n

    Render method, inherited from widget, to render the screen's background.

    May be overridden to customize background visuals.

    "},{"location":"api/app/#textual.app.App.run","title":"run","text":"
    run(\n    *,\n    headless=False,\n    inline=False,\n    inline_no_clear=False,\n    mouse=True,\n    size=None,\n    auto_pilot=None\n)\n

    Run the app.

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output).

    False bool

    Run the app inline (under the prompt).

    False bool

    Don't clear the app output when exiting an inline app.

    False bool

    Enable mouse support.

    True tuple[int, int] | None

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

    None AutopilotCallbackType | None

    An auto pilot coroutine.

    None

    Returns:

    Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run(inline)","title":"inline","text":""},{"location":"api/app/#textual.app.App.run(inline_no_clear)","title":"inline_no_clear","text":""},{"location":"api/app/#textual.app.App.run(mouse)","title":"mouse","text":""},{"location":"api/app/#textual.app.App.run(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run(auto_pilot)","title":"auto_pilot","text":""},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
    run_action(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:

    Name Type Description Default str | ActionParseResult

    Action encoded in a string.

    required DOMNode | None

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

    None

    Returns:

    Type Description bool

    True if the event has been handled.

    "},{"location":"api/app/#textual.app.App.run_action(action)","title":"action","text":""},{"location":"api/app/#textual.app.App.run_action(default_namespace)","title":"default_namespace","text":""},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
    run_async(\n    *,\n    headless=False,\n    inline=False,\n    inline_no_clear=False,\n    mouse=True,\n    size=None,\n    auto_pilot=None\n)\n

    Run the app asynchronously.

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output).

    False bool

    Run the app inline (under the prompt).

    False bool

    Don't clear the app output when exiting an inline app.

    False bool

    Enable mouse support.

    True tuple[int, int] | None

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

    None AutopilotCallbackType | None

    An autopilot coroutine.

    None

    Returns:

    Type Description ReturnType | None

    App return value.

    "},{"location":"api/app/#textual.app.App.run_async(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run_async(inline)","title":"inline","text":""},{"location":"api/app/#textual.app.App.run_async(inline_no_clear)","title":"inline_no_clear","text":""},{"location":"api/app/#textual.app.App.run_async(mouse)","title":"mouse","text":""},{"location":"api/app/#textual.app.App.run_async(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run_async(auto_pilot)","title":"auto_pilot","text":""},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
    run_test(\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:

    ```python\nasync with app.run_test() as pilot:\n    await pilot.click(\"#Button.ok\")\n    assert ...\n```\n

    Parameters:

    Name Type Description Default bool

    Run in headless mode (no output or input).

    True tuple[int, int] | None

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

    (80, 24) bool

    Enable tooltips when testing.

    False bool

    Enable notifications when testing.

    False Callable[[Message], None] | None

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

    None"},{"location":"api/app/#textual.app.App.run_test(headless)","title":"headless","text":""},{"location":"api/app/#textual.app.App.run_test(size)","title":"size","text":""},{"location":"api/app/#textual.app.App.run_test(tooltips)","title":"tooltips","text":""},{"location":"api/app/#textual.app.App.run_test(notifications)","title":"notifications","text":""},{"location":"api/app/#textual.app.App.run_test(message_hook)","title":"message_hook","text":""},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot","text":"
    save_screenshot(filename=None, path=None, time_format=None)\n

    Save an SVG screenshot of the current screen.

    Parameters:

    Name Type Description Default str | None

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

    None str | None

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

    None str | 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.

    None

    Returns:

    Type Description str

    Filename of screenshot.

    "},{"location":"api/app/#textual.app.App.save_screenshot(filename)","title":"filename","text":""},{"location":"api/app/#textual.app.App.save_screenshot(path)","title":"path","text":""},{"location":"api/app/#textual.app.App.save_screenshot(time_format)","title":"time_format","text":""},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus","text":"
    set_focus(widget, scroll_visible=True)\n

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

    Parameters:

    Name Type Description Default Widget | None

    Widget to focus.

    required bool

    Scroll widget in to view.

    True"},{"location":"api/app/#textual.app.App.set_focus(widget)","title":"widget","text":""},{"location":"api/app/#textual.app.App.set_focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/app/#textual.app.App.set_keymap","title":"set_keymap","text":"
    set_keymap(keymap)\n

    Set the keymap, a mapping of binding IDs to key strings.

    Bindings in the keymap are used to override default key bindings, i.e. those defined in BINDINGS class variables.

    Bindings with IDs that are present in the keymap will have their key string replaced with the value from the keymap.

    Parameters:

    Name Type Description Default Keymap

    A mapping of binding IDs to key strings.

    required"},{"location":"api/app/#textual.app.App.set_keymap(keymap)","title":"keymap","text":""},{"location":"api/app/#textual.app.App.simulate_key","title":"simulate_key","text":"
    simulate_key(key)\n

    Simulate a key press.

    This will perform the same action as if the user had pressed the key.

    Parameters:

    Name Type Description Default str

    Key to simulate. May also be the name of a key, e.g. \"space\".

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

    Stop an animation on an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute whose animation should be stopped.

    required bool

    Should the animation be set to its final value?

    True Note

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

    "},{"location":"api/app/#textual.app.App.stop_animation(attribute)","title":"attribute","text":""},{"location":"api/app/#textual.app.App.stop_animation(complete)","title":"complete","text":""},{"location":"api/app/#textual.app.App.suspend","title":"suspend","text":"
    suspend()\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","text":"
    switch_mode(mode)\n

    Switch to a given mode.

    Parameters:

    Name Type Description Default str

    The mode to switch to.

    required

    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_mode(mode)","title":"mode","text":""},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen","text":"
    switch_screen(screen)\n

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

    Parameters:

    Name Type Description Default Screen | str

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

    required"},{"location":"api/app/#textual.app.App.switch_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen","text":"
    uninstall_screen(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:

    Name Type Description Default Screen | str

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

    required

    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.uninstall_screen(screen)","title":"screen","text":""},{"location":"api/app/#textual.app.App.update_keymap","title":"update_keymap","text":"
    update_keymap(keymap)\n

    Update the App's keymap, merging with keymap.

    If a Binding ID exists in both the App's keymap and the keymap argument, the keymap argument takes precedence.

    Parameters:

    Name Type Description Default Keymap

    A mapping of binding IDs to key strings.

    required"},{"location":"api/app/#textual.app.App.update_keymap(keymap)","title":"keymap","text":""},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles","text":"
    update_styles(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","text":"
    validate_sub_title(sub_title)\n

    Make sure the subtitle is set to a string.

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

    Make sure the title is set to a string.

    "},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark","text":"
    watch_dark(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","text":"

    Bases: Exception

    Base class for general App related exceptions.

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

    Bases: ModeError

    Raised if there is an issue with a mode name.

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

    Bases: Exception

    Base class for exceptions related to modes.

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

    Bases: Exception

    Base class for exceptions that relate to screens.

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

    Bases: ScreenError

    Raised when trying to manipulate the screen stack incorrectly.

    "},{"location":"api/app/#textual.app.SuspendNotSupported","title":"SuspendNotSupported","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.SystemCommand","title":"SystemCommand","text":"

    Bases: NamedTuple

    Defines a system command used in the command palette (yielded from get_system_commands).

    "},{"location":"api/app/#textual.app.SystemCommand.callback","title":"callback instance-attribute","text":"
    callback\n

    A callback to invoke when the command is selected.

    "},{"location":"api/app/#textual.app.SystemCommand.discover","title":"discover class-attribute instance-attribute","text":"
    discover = True\n

    Should the command show when the search is empty?

    "},{"location":"api/app/#textual.app.SystemCommand.help","title":"help instance-attribute","text":"
    help\n

    Additional help text, shown under the title.

    "},{"location":"api/app/#textual.app.SystemCommand.title","title":"title instance-attribute","text":"
    title\n

    The title of the command (used in search).

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

    Bases: ModeError

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

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

    Callable to lazy load the system commands.

    Returns:

    Type Description type[SystemCommandsProvider]

    System commands class.

    "},{"location":"api/await_complete/","title":"textual.await_complete","text":"

    This module contains the AwaitComplete class. An AwaitComplete object is returned by methods that do work in the background. You can await this object if you need to know when that work has completed. Or you can ignore it, and Textual will automatically await the work before handling the next message.

    Note

    You are unlikely to need to explicitly create these objects yourself.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete","title":"AwaitComplete","text":"
    AwaitComplete(*awaitables, pre_await=None)\n

    An 'optionally-awaitable' object which runs one or more coroutines (or other awaitables) concurrently.

    Parameters:

    Name Type Description Default Awaitable

    One or more awaitables to run concurrently.

    ()"},{"location":"api/await_complete/#textual.await_complete.AwaitComplete(awaitables)","title":"awaitables","text":""},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.exception","title":"exception property","text":"
    exception\n

    An exception if the awaitables failed.

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

    True if the task has completed.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.call_next","title":"call_next","text":"
    call_next(node)\n

    Await after the next message.

    Parameters:

    Name Type Description Default MessagePump

    The node which created the object.

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

    Returns an already completed instance of AwaitComplete.

    "},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.set_pre_await_callback","title":"set_pre_await_callback","text":"
    set_pre_await_callback(pre_await)\n

    Set a callback to run prior to awaiting.

    This is used by Textual, mainly to check for possible deadlocks. You are unlikely to need to call this method in an app.

    Parameters:

    Name Type Description Default CallbackType | None

    A callback.

    required"},{"location":"api/await_complete/#textual.await_complete.AwaitComplete.set_pre_await_callback(pre_await)","title":"pre_await","text":""},{"location":"api/await_remove/","title":"textual.await_remove","text":"

    This module contains the AwaitRemove class. An AwaitRemove object is returned by Widget.remove() and other methods which remove widgets. You can await the return value if you need to know exactly when the widget(s) have been removed. Or you can ignore it and Textual will wait for the widgets to be removed before handling the next message.

    Note

    You are unlikely to need to explicitly create these objects yourself.

    An optionally awaitable object returned by methods that remove widgets.

    "},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove","text":"
    AwaitRemove(tasks, post_remove=None)\n

    An awaitable that waits for nodes to be removed.

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

    This module contains the Binding class and related objects.

    See bindings in the guide for details.

    "},{"location":"api/binding/#textual.binding.BindingIDString","title":"BindingIDString module-attribute","text":"
    BindingIDString = str\n

    The ID of a Binding defined somewhere in the application.

    Corresponds to the id parameter of the Binding class.

    "},{"location":"api/binding/#textual.binding.BindingType","title":"BindingType module-attribute","text":"
    BindingType = (\n    \"Binding | tuple[str, str] | tuple[str, str, str]\"\n)\n

    The possible types of a binding found in the BINDINGS class variable.

    "},{"location":"api/binding/#textual.binding.KeyString","title":"KeyString module-attribute","text":"
    KeyString = str\n

    A string that represents a key binding.

    For example, \"x\", \"ctrl+i\", \"ctrl+shift+a\", \"ctrl+j,space,x\", etc.

    "},{"location":"api/binding/#textual.binding.Keymap","title":"Keymap module-attribute","text":"
    Keymap = Mapping[BindingIDString, KeyString]\n

    A mapping of binding IDs to key strings, used for overriding default key bindings.

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

    Bases: NamedTuple

    Information about an active binding (returned from active_bindings).

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

    The binding information.

    "},{"location":"api/binding/#textual.binding.ActiveBinding.enabled","title":"enabled instance-attribute","text":"
    enabled\n

    Is the binding enabled? (enabled bindings are typically rendered dim)

    "},{"location":"api/binding/#textual.binding.ActiveBinding.node","title":"node instance-attribute","text":"
    node\n

    The node where the binding is defined.

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

    Optional tooltip shown in Footer.

    "},{"location":"api/binding/#textual.binding.Binding","title":"Binding dataclass","text":"
    Binding(\n    key,\n    action,\n    description=\"\",\n    show=True,\n    key_display=None,\n    priority=False,\n    tooltip=\"\",\n    id=None,\n)\n

    The configuration of a key binding.

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

    Action to bind to.

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

    Description of action.

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

    ID of the binding. Intended to be globally unique, but uniqueness is not enforced.

    If specified in the App's keymap then Textual will use this ID to lookup the binding, and substitute the key property of the Binding with the key specified in the keymap.

    "},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
    key\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 = None\n

    How the key should be shown in footer.

    If None, the display of the key will use the result of App.get_key_display.

    If overridden in a keymap then this value is ignored.

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

    Enable priority binding for this key.

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

    Show the action in Footer, or False to hide.

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

    Optional tooltip to show in footer.

    "},{"location":"api/binding/#textual.binding.Binding.make_bindings","title":"make_bindings classmethod","text":"
    make_bindings(bindings)\n

    Convert a list of BindingType (the types that can be specified in BINDINGS) into an Iterable[Binding].

    Compound bindings like \"j,down\" will be expanded into 2 Binding instances.

    Parameters:

    Name Type Description Default Iterable[BindingType]

    An iterable of BindingType.

    required

    Returns:

    Type Description Iterable[Binding]

    An iterable of Binding.

    "},{"location":"api/binding/#textual.binding.Binding.make_bindings(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.Binding.parse_key","title":"parse_key","text":"
    parse_key()\n

    Parse a key in to a list of modifiers, and the actual key.

    Returns:

    Type Description tuple[list[str], str]

    A tuple of (MODIFIER LIST, KEY).

    "},{"location":"api/binding/#textual.binding.Binding.with_key","title":"with_key","text":"
    with_key(key, key_display=None)\n

    Return a new binding with the key and key_display set to the specified values.

    Parameters:

    Name Type Description Default str

    The new key to set.

    required str | None

    The new key display to set.

    None

    Returns:

    Type Description Binding

    A new binding with the key set to the specified value.

    "},{"location":"api/binding/#textual.binding.Binding.with_key(key)","title":"key","text":""},{"location":"api/binding/#textual.binding.Binding.with_key(key_display)","title":"key_display","text":""},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError","text":"

    Bases: Exception

    A binding related error.

    "},{"location":"api/binding/#textual.binding.BindingsMap","title":"BindingsMap","text":"
    BindingsMap(bindings=None)\n

    Manage a set of bindings.

    Parameters:

    Name Type Description Default Iterable[BindingType] | None

    An optional set of initial bindings.

    None Note

    The iterable of bindings can contain either a Binding instance, or a tuple of 3 values mapping to the first three properties of a Binding.

    "},{"location":"api/binding/#textual.binding.BindingsMap(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.BindingsMap.key_to_bindings","title":"key_to_bindings instance-attribute","text":"
    key_to_bindings = {}\n

    Mapping of key (e.g. \"ctrl+a\") to list of bindings for that key.

    "},{"location":"api/binding/#textual.binding.BindingsMap.shown_keys","title":"shown_keys property","text":"
    shown_keys\n

    A list of bindings for shown keys.

    "},{"location":"api/binding/#textual.binding.BindingsMap.apply_keymap","title":"apply_keymap","text":"
    apply_keymap(keymap)\n

    Replace bindings for keys that are present in keymap.

    Preserves existing bindings for keys that are not in keymap.

    Parameters:

    Name Type Description Default Keymap

    A keymap to overlay.

    required

    Returns:

    Name Type Description KeymapApplyResult KeymapApplyResult

    The result of applying the keymap, including any clashed bindings.

    "},{"location":"api/binding/#textual.binding.BindingsMap.apply_keymap(keymap)","title":"keymap","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind","title":"bind","text":"
    bind(\n    keys,\n    action,\n    description=\"\",\n    show=True,\n    key_display=None,\n    priority=False,\n)\n

    Bind keys to an action.

    Parameters:

    Name Type Description Default str

    The keys to bind. Can be a comma-separated list of keys.

    required str

    The action to bind the keys to.

    required str

    An optional description for the binding.

    '' bool

    A flag to say if the binding should appear in the footer.

    True str | None

    Optional string to display in the footer for the key.

    None bool

    Is this a priority binding, checked form app down to focused widget?

    False"},{"location":"api/binding/#textual.binding.BindingsMap.bind(keys)","title":"keys","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(action)","title":"action","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(description)","title":"description","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(show)","title":"show","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(key_display)","title":"key_display","text":""},{"location":"api/binding/#textual.binding.BindingsMap.bind(priority)","title":"priority","text":""},{"location":"api/binding/#textual.binding.BindingsMap.copy","title":"copy","text":"
    copy()\n

    Return a copy of this instance.

    Return

    New bindings object.

    "},{"location":"api/binding/#textual.binding.BindingsMap.from_keys","title":"from_keys classmethod","text":"
    from_keys(keys)\n

    Construct a BindingsMap from a dict of keys and bindings.

    Parameters:

    Name Type Description Default dict[str, list[Binding]]

    A dict that maps a key on to a list of Binding objects.

    required

    Returns:

    Type Description BindingsMap

    New BindingsMap

    "},{"location":"api/binding/#textual.binding.BindingsMap.from_keys(keys)","title":"keys","text":""},{"location":"api/binding/#textual.binding.BindingsMap.get_bindings_for_key","title":"get_bindings_for_key","text":"
    get_bindings_for_key(key)\n

    Get a list of bindings for a given key.

    Parameters:

    Name Type Description Default str

    Key to look up.

    required

    Raises:

    Type Description NoBinding

    If the binding does not exist.

    Returns:

    Type Description list[Binding]

    A list of bindings associated with the key.

    "},{"location":"api/binding/#textual.binding.BindingsMap.get_bindings_for_key(key)","title":"key","text":""},{"location":"api/binding/#textual.binding.BindingsMap.merge","title":"merge classmethod","text":"
    merge(bindings)\n

    Merge a bindings.

    Parameters:

    Name Type Description Default Iterable[BindingsMap]

    A number of bindings.

    required

    Returns:

    Type Description BindingsMap

    New BindingsMap.

    "},{"location":"api/binding/#textual.binding.BindingsMap.merge(bindings)","title":"bindings","text":""},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding","text":"

    Bases: Exception

    Binding key is in an invalid format.

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

    Bases: NamedTuple

    The result of applying a keymap.

    "},{"location":"api/binding/#textual.binding.KeymapApplyResult.clashed_bindings","title":"clashed_bindings instance-attribute","text":"
    clashed_bindings\n

    A list of bindings that were clashed and replaced by the keymap.

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

    Bases: Exception

    A binding was not found.

    "},{"location":"api/cache/","title":"textual.cache","text":"

    Cache classes are dict-like containers used to avoid recalculating expensive operations such as rendering.

    You can also use them in your own apps for similar reasons.

    "},{"location":"api/cache/#textual.cache.FIFOCache","title":"FIFOCache","text":"
    FIFOCache(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:

    Name Type Description Default int

    Maximum size of cache before discarding items.

    required"},{"location":"api/cache/#textual.cache.FIFOCache(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.FIFOCache.clear","title":"clear","text":"
    clear()\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.FIFOCache.get","title":"get","text":"
    get(key: CacheKey) -> CacheValue | None\n
    get(\n    key: CacheKey, default: DefaultValue\n) -> CacheValue | DefaultValue\n
    get(key, default=None)\n

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

    Parameters:

    Name Type Description Default CacheKey

    Key

    required DefaultValue | None

    Default to return if key is not present.

    None

    Returns:

    Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.FIFOCache.get(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.FIFOCache.get(default)","title":"default","text":""},{"location":"api/cache/#textual.cache.FIFOCache.keys","title":"keys","text":"
    keys()\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.FIFOCache.set","title":"set","text":"
    set(key, value)\n

    Set a value.

    Parameters:

    Name Type Description Default CacheKey

    Key.

    required CacheValue

    Value.

    required"},{"location":"api/cache/#textual.cache.FIFOCache.set(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.FIFOCache.set(value)","title":"value","text":""},{"location":"api/cache/#textual.cache.LRUCache","title":"LRUCache","text":"
    LRUCache(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:

    Name Type Description Default int

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

    required"},{"location":"api/cache/#textual.cache.LRUCache(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.LRUCache.maxsize","title":"maxsize property writable","text":"
    maxsize\n
    "},{"location":"api/cache/#textual.cache.LRUCache.clear","title":"clear","text":"
    clear()\n

    Clear the cache.

    "},{"location":"api/cache/#textual.cache.LRUCache.discard","title":"discard","text":"
    discard(key)\n

    Discard item in cache from key.

    Parameters:

    Name Type Description Default CacheKey

    Cache key.

    required"},{"location":"api/cache/#textual.cache.LRUCache.discard(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.get","title":"get","text":"
    get(key: CacheKey) -> CacheValue | None\n
    get(\n    key: CacheKey, default: DefaultValue\n) -> CacheValue | DefaultValue\n
    get(key, default=None)\n

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

    Parameters:

    Name Type Description Default CacheKey

    Key

    required DefaultValue | None

    Default to return if key is not present.

    None

    Returns:

    Type Description CacheValue | DefaultValue | None

    Either the value or a default.

    "},{"location":"api/cache/#textual.cache.LRUCache.get(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.get(default)","title":"default","text":""},{"location":"api/cache/#textual.cache.LRUCache.grow","title":"grow","text":"
    grow(maxsize)\n

    Grow the maximum size to at least maxsize elements.

    Parameters:

    Name Type Description Default int

    New maximum size.

    required"},{"location":"api/cache/#textual.cache.LRUCache.grow(maxsize)","title":"maxsize","text":""},{"location":"api/cache/#textual.cache.LRUCache.keys","title":"keys","text":"
    keys()\n

    Get cache keys.

    "},{"location":"api/cache/#textual.cache.LRUCache.set","title":"set","text":"
    set(key, value)\n

    Set a value.

    Parameters:

    Name Type Description Default CacheKey

    Key.

    required CacheValue

    Value.

    required"},{"location":"api/cache/#textual.cache.LRUCache.set(key)","title":"key","text":""},{"location":"api/cache/#textual.cache.LRUCache.set(value)","title":"value","text":""},{"location":"api/color/","title":"textual.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 = Color(0, 0, 0)\n

    A constant for pure black.

    "},{"location":"api/color/#textual.color.TRANSPARENT","title":"TRANSPARENT module-attribute","text":"
    TRANSPARENT = parse('transparent')\n

    A constant for transparent.

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

    A constant for pure white.

    "},{"location":"api/color/#textual.color.Color","title":"Color","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 = 1.0\n

    Alpha (opacity) component in range 0 to 1.

    "},{"location":"api/color/#textual.color.Color.ansi","title":"ansi class-attribute instance-attribute","text":"
    ansi = None\n

    ANSI color index. -1 means default color. None if not an ANSI color.

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

    Blue component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
    brightness\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\n

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

    "},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
    css\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\n

    Green component in range 0 to 255.

    "},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
    hex\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\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\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\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\n

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

    "},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
    monochrome\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\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\n

    Red component in range 0 to 255.

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

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

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

    This color encoded in Rich's Color class.

    Returns:

    Type Description Color

    A color object as used by Rich.

    "},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
    blend(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:

    Name Type Description Default Color

    Another color.

    required float

    A blend factor, 0 -> 1.

    required float | None

    New alpha for result.

    None

    Returns:

    Type Description Color

    A new color.

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

    Darken the color by a given amount.

    Parameters:

    Name Type Description Default float

    Value between 0-1 to reduce luminance by.

    required float | None

    Alpha component for new color or None to copy alpha.

    None

    Returns:

    Type Description Color

    New color.

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

    Create a color from HLS components.

    Parameters:

    Name Type Description Default float

    Hue.

    required float

    Lightness.

    required float

    Saturation.

    required

    Returns:

    Type Description Color

    A new color.

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

    Create a new color from Rich's Color class.

    Parameters:

    Name Type Description Default Color

    An instance of Rich color.

    required

    Returns:

    Type Description Color

    A new Color instance.

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

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

    Parameters:

    Name Type Description Default float

    An alpha value to apply to the result.

    0.95

    Returns:

    Type Description Color

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

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

    Lighten the color by a given amount.

    Parameters:

    Name Type Description Default float

    Value between 0-1 to increase luminance by.

    required float | None

    Alpha component for new color or None to copy alpha.

    None

    Returns:

    Type Description Color

    New color.

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

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

    Parameters:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.multiply_alpha(alpha)","title":"alpha","text":""},{"location":"api/color/#textual.color.Color.parse","title":"parse cached classmethod","text":"
    parse(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:

    Name Type Description Default str | Color

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

    required

    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.parse(color_text)","title":"color_text","text":""},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha","text":"
    with_alpha(alpha)\n

    Create a new color with the given alpha.

    Parameters:

    Name Type Description Default float

    New value for alpha.

    required

    Returns:

    Type Description Color

    A new color.

    "},{"location":"api/color/#textual.color.Color.with_alpha(alpha)","title":"alpha","text":""},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError","text":"
    ColorParseError(message, suggested_color=None)\n

    Bases: Exception

    A color failed to parse.

    Parameters:

    Name Type Description Default str

    The error message

    required str | None

    A close color we can suggest.

    None"},{"location":"api/color/#textual.color.ColorParseError(message)","title":"message","text":""},{"location":"api/color/#textual.color.ColorParseError(suggested_color)","title":"suggested_color","text":""},{"location":"api/color/#textual.color.Gradient","title":"Gradient","text":"
    Gradient(*stops, quality=50)\n

    Defines a color gradient.

    A gradient is defined by a sequence of \"stops\" consisting of a tuple containing a float and a color. The stop indicates the color at that point on a spectrum between 0 and 1. Colors may be given as a Color instance, or a string that can be parsed into a Color (with Color.parse).

    The quality argument defines the number of steps in the gradient. Intermediate colors are interpolated from the two nearest colors. Increasing quality can generate a smoother looking gradient, at the expense of a little extra work to pre-calculate the colors.

    Parameters:

    Name Type Description Default tuple[float, Color | str]

    Color stops.

    () int

    The number of steps in the gradient.

    50

    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(stops)","title":"stops","text":""},{"location":"api/color/#textual.color.Gradient(quality)","title":"quality","text":""},{"location":"api/color/#textual.color.Gradient.colors","title":"colors property","text":"
    colors\n

    A list of colors in the gradient.

    "},{"location":"api/color/#textual.color.Gradient.from_colors","title":"from_colors classmethod","text":"
    from_colors(*colors, quality=50)\n

    Construct a gradient form a sequence of colors, where the stops are evenly spaced.

    Parameters:

    Name Type Description Default Color | str

    Positional arguments may be Color instances or strings to parse into a color.

    () int

    The number of steps in the gradient.

    50

    Returns:

    Type Description Gradient

    A new Gradient instance.

    "},{"location":"api/color/#textual.color.Gradient.from_colors(*colors)","title":"*colors","text":""},{"location":"api/color/#textual.color.Gradient.from_colors(quality)","title":"quality","text":""},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color","text":"
    get_color(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:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A Textual color.

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

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

    Positions that are between stops will return a blended color.

    Parameters:

    Name Type Description Default float

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

    required

    Returns:

    Type Description Color

    A (Rich) color.

    "},{"location":"api/color/#textual.color.Gradient.get_rich_color(position)","title":"position","text":""},{"location":"api/color/#textual.color.HSL","title":"HSL","text":"

    Bases: NamedTuple

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

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

    HSL in css format.

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

    Hue in range 0 to 1.

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

    Lightness in range 0 to 1.

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

    Saturation in range 0 to 1.

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

    Bases: NamedTuple

    A color in HSV (Hue, Saturation, Value) format.

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

    Hue in range 0 to 1.

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

    Saturation in range 0 to 1.

    "},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
    v\n

    Value un range 0 to 1.

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

    Bases: NamedTuple

    A color in CIE-L*ab format.

    "},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
    L\n

    Lightness in range 0 to 100.

    "},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
    a\n

    A axis in range -127 to 128.

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

    B axis in range -127 to 128.

    "},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb","text":"
    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","text":"
    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":"textual.command","text":"

    This module contains classes for working with Textual's command palette.

    See the guide on the Command Palette for full details.

    "},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
    Hits = AsyncIterator['DiscoveryHit | Hit']\n

    Return type for the command provider's search method.

    "},{"location":"api/command/#textual.command.Command","title":"Command","text":"
    Command(prompt, hit, id=None, disabled=False)\n

    Bases: Option

    Class that holds a hit in the CommandList.

    Parameters:

    Name Type Description Default RenderableType

    The prompt for the option.

    required DiscoveryHit | Hit

    The details of the hit associated with the option.

    required str | None

    The optional ID for the option.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"api/command/#textual.command.Command(prompt)","title":"prompt","text":""},{"location":"api/command/#textual.command.Command(hit)","title":"hit","text":""},{"location":"api/command/#textual.command.Command(id)","title":"id","text":""},{"location":"api/command/#textual.command.Command(disabled)","title":"disabled","text":""},{"location":"api/command/#textual.command.Command.hit","title":"hit instance-attribute","text":"
    hit = hit\n

    The details of the hit associated with the option.

    "},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput","text":"
    CommandInput(\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    tooltip=None\n)\n

    Bases: Input

    The command palette input control.

    Parameters:

    Name Type Description Default str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Highlighter | None

    An optional highlighter for the input.

    None bool

    Flag to say if the field should obfuscate its content.

    False str | None

    A regex to restrict character inputs.

    None InputType

    The type of the input.

    'text' int

    The maximum length of the input, or 0 for no maximum length.

    0 Suggester | None

    Suggester associated with this input instance.

    None Validator | Iterable[Validator] | None

    An iterable of validators that the Input value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"api/command/#textual.command.CommandInput(value)","title":"value","text":""},{"location":"api/command/#textual.command.CommandInput(placeholder)","title":"placeholder","text":""},{"location":"api/command/#textual.command.CommandInput(highlighter)","title":"highlighter","text":""},{"location":"api/command/#textual.command.CommandInput(password)","title":"password","text":""},{"location":"api/command/#textual.command.CommandInput(restrict)","title":"restrict","text":""},{"location":"api/command/#textual.command.CommandInput(type)","title":"type","text":""},{"location":"api/command/#textual.command.CommandInput(max_length)","title":"max_length","text":""},{"location":"api/command/#textual.command.CommandInput(suggester)","title":"suggester","text":""},{"location":"api/command/#textual.command.CommandInput(validators)","title":"validators","text":""},{"location":"api/command/#textual.command.CommandInput(validate_on)","title":"validate_on","text":""},{"location":"api/command/#textual.command.CommandInput(valid_empty)","title":"valid_empty","text":""},{"location":"api/command/#textual.command.CommandInput(name)","title":"name","text":""},{"location":"api/command/#textual.command.CommandInput(id)","title":"id","text":""},{"location":"api/command/#textual.command.CommandInput(classes)","title":"classes","text":""},{"location":"api/command/#textual.command.CommandInput(disabled)","title":"disabled","text":""},{"location":"api/command/#textual.command.CommandInput(tooltip)","title":"tooltip","text":""},{"location":"api/command/#textual.command.CommandList","title":"CommandList","text":"
    CommandList(\n    *content,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    wrap=True,\n    tooltip=None,\n)\n

    Bases: OptionList

    The command palette command list.

    "},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette","text":"
    CommandPalette()\n

    Bases: SystemModalScreen

    The Textual command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"ctrl+end, shift+end\",\n        \"command_list('last')\",\n        \"Go to bottom\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+home, shift+home\",\n        \"command_list('first')\",\n        \"Go to top\",\n        show=False,\n    ),\n    Binding(\n        \"down\", \"cursor_down\", \"Next command\", show=False\n    ),\n    Binding(\"escape\", \"escape\", \"Exit the command palette\"),\n    Binding(\n        \"pagedown\",\n        \"command_list('page_down')\",\n        \"Next page\",\n        show=False,\n    ),\n    Binding(\n        \"pageup\",\n        \"command_list('page_up')\",\n        \"Previous page\",\n        show=False,\n    ),\n    Binding(\n        \"up\",\n        \"command_list('cursor_up')\",\n        \"Previous command\",\n        show=False,\n    ),\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 = {\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 = 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.Closed","title":"Closed dataclass","text":"
    Closed(option_selected)\n

    Bases: Message

    Posted to App when the command palette is closed.

    "},{"location":"api/command/#textual.command.CommandPalette.Closed.option_selected","title":"option_selected instance-attribute","text":"
    option_selected\n

    True if an option was selected, False if the palette was closed without selecting an option.

    "},{"location":"api/command/#textual.command.CommandPalette.Opened","title":"Opened dataclass","text":"
    Opened()\n

    Bases: Message

    Posted to App when the command palette is opened.

    "},{"location":"api/command/#textual.command.CommandPalette.OptionHighlighted","title":"OptionHighlighted dataclass","text":"
    OptionHighlighted(highlighted_event)\n

    Bases: Message

    Posted to App when an option is highlighted in the command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.OptionHighlighted.highlighted_event","title":"highlighted_event instance-attribute","text":"
    highlighted_event\n

    The option highlighted event from the OptionList within the command palette.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
    is_open(app)\n

    Is the command palette current open?

    Parameters:

    Name Type Description Default App

    The app to test.

    required

    Returns:

    Type Description bool

    True if the command palette is currently open, False if not.

    "},{"location":"api/command/#textual.command.CommandPalette.is_open(app)","title":"app","text":""},{"location":"api/command/#textual.command.DiscoveryHit","title":"DiscoveryHit dataclass","text":"
    DiscoveryHit(display, command, text=None, help=None)\n

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.DiscoveryHit.command","title":"command instance-attribute","text":"
    command\n

    The function to call when the command is chosen.

    "},{"location":"api/command/#textual.command.DiscoveryHit.display","title":"display instance-attribute","text":"
    display\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.DiscoveryHit.help","title":"help class-attribute instance-attribute","text":"
    help = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.DiscoveryHit.prompt","title":"prompt property","text":"
    prompt\n

    The prompt to use when displaying the discovery hit in the command palette.

    "},{"location":"api/command/#textual.command.DiscoveryHit.score","title":"score property","text":"
    score\n

    A discovery hit always has a score of 0.

    The order in which discovery hits are displayed is determined by the order in which they are yielded by the Provider. It's up to the developer to yield DiscoveryHits in the .

    "},{"location":"api/command/#textual.command.DiscoveryHit.text","title":"text class-attribute instance-attribute","text":"
    text = None\n

    The command text associated with the hit, as plain text.

    If display is not simple text, this attribute should be provided by the Provider object.

    "},{"location":"api/command/#textual.command.Hit","title":"Hit dataclass","text":"
    Hit(score, match_display, command, text=None, help=None)\n

    Holds the details of a single command search hit.

    "},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
    command\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 = None\n

    Optional help text for the command.

    "},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
    match_display\n

    A string or Rich renderable representation of the hit.

    "},{"location":"api/command/#textual.command.Hit.prompt","title":"prompt property","text":"
    prompt\n

    The prompt to use when displaying the hit in the command palette.

    "},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
    score\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 = 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","text":"
    Matcher(query, *, match_style=None, case_sensitive=False)\n

    A fuzzy matcher.

    Parameters:

    Name Type Description Default str

    A query as typed in by the user.

    required Style | None

    The style to use to highlight matched portions of a string.

    None bool

    Should matching be case sensitive?

    False"},{"location":"api/command/#textual.command.Matcher(query)","title":"query","text":""},{"location":"api/command/#textual.command.Matcher(match_style)","title":"match_style","text":""},{"location":"api/command/#textual.command.Matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/command/#textual.command.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive\n

    Is this matcher case sensitive?

    "},{"location":"api/command/#textual.command.Matcher.match_style","title":"match_style property","text":"
    match_style\n

    The style that will be used to highlight hits in the matched text.

    "},{"location":"api/command/#textual.command.Matcher.query","title":"query property","text":"
    query\n

    The query string to look for.

    "},{"location":"api/command/#textual.command.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern\n

    The regular expression pattern built from the query.

    "},{"location":"api/command/#textual.command.Matcher.highlight","title":"highlight","text":"
    highlight(candidate)\n

    Highlight the candidate with the fuzzy match.

    Parameters:

    Name Type Description Default str

    The candidate string to match against the query.

    required

    Returns:

    Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/command/#textual.command.Matcher.highlight(candidate)","title":"candidate","text":""},{"location":"api/command/#textual.command.Matcher.match","title":"match","text":"
    match(candidate)\n

    Match the candidate against the query.

    Parameters:

    Name Type Description Default str

    Candidate string to match against the query.

    required

    Returns:

    Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/command/#textual.command.Matcher.match(candidate)","title":"candidate","text":""},{"location":"api/command/#textual.command.Provider","title":"Provider","text":"
    Provider(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:

    Name Type Description Default Screen[Any]

    A reference to the active screen.

    required"},{"location":"api/command/#textual.command.Provider(screen)","title":"screen","text":""},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
    app\n

    A reference to the application.

    "},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
    focused\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\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\n

    The currently-active screen in the application.

    "},{"location":"api/command/#textual.command.Provider.discover","title":"discover async","text":"
    discover()\n

    A default collection of hits for the provider.

    Yields:

    Type Description Hits

    Instances of DiscoveryHit.

    Note

    This is different from search in that it should yield DiscoveryHits that should be shown by default (before user input).

    It is permitted to not implement this method.

    "},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher","text":"
    matcher(user_input, case_sensitive=False)\n

    Create a fuzzy matcher for the given user input.

    Parameters:

    Name Type Description Default str

    The text that the user has input.

    required bool

    Should matching be case sensitive?

    False

    Returns:

    Type Description Matcher

    A fuzzy matcher object for matching against candidate hits.

    "},{"location":"api/command/#textual.command.Provider.matcher(user_input)","title":"user_input","text":""},{"location":"api/command/#textual.command.Provider.matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/command/#textual.command.Provider.search","title":"search abstractmethod async","text":"
    search(query)\n

    A request to search for commands relevant to the given query.

    Parameters:

    Name Type Description Default str

    The user input to be matched.

    required

    Yields:

    Type Description Hits

    Instances of Hit.

    "},{"location":"api/command/#textual.command.Provider.search(query)","title":"query","text":""},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
    shutdown()\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":"
    startup()\n

    Called after the Provider is initialized, but before any calls to search.

    "},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon","text":"
    SearchIcon(\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: 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('\ud83d\udd0e')\n

    The icon to display.

    "},{"location":"api/constants/","title":"textual.constants","text":"

    This module contains constants, which may be set in environment variables.

    "},{"location":"api/constants/#textual.constants.COLOR_SYSTEM","title":"COLOR_SYSTEM module-attribute","text":"
    COLOR_SYSTEM = get_environ('TEXTUAL_COLOR_SYSTEM', 'auto')\n

    Force color system override.

    "},{"location":"api/constants/#textual.constants.DEBUG","title":"DEBUG module-attribute","text":"
    DEBUG = _get_environ_bool('TEXTUAL_DEBUG')\n

    Enable debug mode.

    "},{"location":"api/constants/#textual.constants.DEVTOOLS_HOST","title":"DEVTOOLS_HOST module-attribute","text":"
    DEVTOOLS_HOST = get_environ(\n    \"TEXTUAL_DEVTOOLS_HOST\", \"127.0.0.1\"\n)\n

    The host where textual console is running.

    "},{"location":"api/constants/#textual.constants.DEVTOOLS_PORT","title":"DEVTOOLS_PORT module-attribute","text":"
    DEVTOOLS_PORT = _get_environ_int(\n    \"TEXTUAL_DEVTOOLS_PORT\", 8081\n)\n

    Constant with the port that the devtools will connect to.

    "},{"location":"api/constants/#textual.constants.DRIVER","title":"DRIVER module-attribute","text":"
    DRIVER = get_environ('TEXTUAL_DRIVER', None)\n

    Import for replacement driver.

    "},{"location":"api/constants/#textual.constants.ESCAPE_DELAY","title":"ESCAPE_DELAY module-attribute","text":"
    ESCAPE_DELAY = _get_environ_int('ESCDELAY', 100) / 1000.0\n

    The delay (in seconds) before reporting an escape key (not used if the extend key protocol is available).

    "},{"location":"api/constants/#textual.constants.FILTERS","title":"FILTERS module-attribute","text":"
    FILTERS = get_environ('TEXTUAL_FILTERS', '')\n

    A list of filters to apply to renderables.

    "},{"location":"api/constants/#textual.constants.LOG_FILE","title":"LOG_FILE module-attribute","text":"
    LOG_FILE = get_environ('TEXTUAL_LOG', None)\n

    A last resort log file that appends all logs, when devtools isn't working.

    "},{"location":"api/constants/#textual.constants.MAX_FPS","title":"MAX_FPS module-attribute","text":"
    MAX_FPS = _get_environ_int('TEXTUAL_FPS', 60)\n

    Maximum frames per second for updates.

    "},{"location":"api/constants/#textual.constants.PRESS","title":"PRESS module-attribute","text":"
    PRESS = get_environ('TEXTUAL_PRESS', '')\n

    Keys to automatically press.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_DELAY","title":"SCREENSHOT_DELAY module-attribute","text":"
    SCREENSHOT_DELAY = _get_environ_int(\n    \"TEXTUAL_SCREENSHOT\", -1\n)\n

    Seconds delay before taking screenshot.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_FILENAME","title":"SCREENSHOT_FILENAME module-attribute","text":"
    SCREENSHOT_FILENAME = get_environ(\n    \"TEXTUAL_SCREENSHOT_FILENAME\"\n)\n

    The filename to use for the screenshot.

    "},{"location":"api/constants/#textual.constants.SCREENSHOT_LOCATION","title":"SCREENSHOT_LOCATION module-attribute","text":"
    SCREENSHOT_LOCATION = get_environ(\n    \"TEXTUAL_SCREENSHOT_LOCATION\"\n)\n

    The location where screenshots should be written.

    "},{"location":"api/constants/#textual.constants.SHOW_RETURN","title":"SHOW_RETURN module-attribute","text":"
    SHOW_RETURN = _get_environ_bool('TEXTUAL_SHOW_RETURN')\n

    Write the return value on exit.

    "},{"location":"api/constants/#textual.constants.SLOW_THRESHOLD","title":"SLOW_THRESHOLD module-attribute","text":"
    SLOW_THRESHOLD = _get_environ_int(\n    \"TEXTUAL_SLOW_THRESHOLD\", 500\n)\n

    The time threshold (in milliseconds) after which a warning is logged if message processing exceeds this duration.

    "},{"location":"api/constants/#textual.constants.TEXTUAL_ANIMATIONS","title":"TEXTUAL_ANIMATIONS module-attribute","text":"
    TEXTUAL_ANIMATIONS = _get_textual_animations()\n

    Determines whether animations run or not.

    "},{"location":"api/containers/","title":"textual.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","text":"
    Center(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container which aligns children on the X axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Center(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Center(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Center(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Center(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Center(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Container","title":"Container","text":"
    Container(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    Simple container widget, with vertical layout.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Container(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Container(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Container(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Container(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Container(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Grid","title":"Grid","text":"
    Grid(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with grid layout.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Grid(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Grid(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Grid(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Grid(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Grid(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal","text":"
    Horizontal(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with horizontal layout and no scrollbars.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Horizontal(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Horizontal(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Horizontal(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Horizontal(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Horizontal(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll","text":"
    HorizontalScroll(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A container with horizontal layout and an automatic scrollbar on the X axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.HorizontalScroll(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.HorizontalScroll(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.Middle","title":"Middle","text":"
    Middle(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container which aligns children on the Y axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Middle(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Middle(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Middle(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Middle(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Middle(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer","text":"
    ScrollableContainer(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A scrollable container with vertical layout, and auto scrollbars on both axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.ScrollableContainer(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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    Binding(\n        \"ctrl+pageup\", \"page_left\", \"Page Left\", show=False\n    ),\n    Binding(\n        \"ctrl+pagedown\",\n        \"page_right\",\n        \"Page Right\",\n        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. ctrl+pageup Scroll left one page, if horizontal scrolling is available. ctrl+pagedown Scroll right one page, if horizontal scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical","text":"
    Vertical(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: Widget

    A container with vertical layout and no scrollbars.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.Vertical(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.Vertical(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.Vertical(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.Vertical(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.Vertical(disabled)","title":"disabled","text":""},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll","text":"
    VerticalScroll(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A container with vertical layout and an automatic scrollbar on the Y axis.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/containers/#textual.containers.VerticalScroll(*children)","title":"*children","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(name)","title":"name","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(id)","title":"id","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(classes)","title":"classes","text":""},{"location":"api/containers/#textual.containers.VerticalScroll(disabled)","title":"disabled","text":""},{"location":"api/coordinate/","title":"textual.coordinate","text":"

    A class to store a coordinate, used by the DataTable.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate","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\n

    The column of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
    row\n

    The row of the coordinate within a grid.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down","text":"
    down()\n

    Get the coordinate below.

    Returns:

    Type Description Coordinate

    The coordinate below.

    "},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left","text":"
    left()\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","text":"
    right()\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","text":"
    up()\n

    Get the coordinate above.

    Returns:

    Type Description Coordinate

    The coordinate above.

    "},{"location":"api/dom_node/","title":"textual.dom","text":"

    The module contains DOMNode, the base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

    "},{"location":"api/dom_node/#textual.dom.QueryOneCacheKey","title":"QueryOneCacheKey module-attribute","text":"
    QueryOneCacheKey = 'tuple[int, str, Type[Widget] | None]'\n

    The key used to cache query_one results.

    "},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
    WalkMethod = Literal['depth', 'breadth']\n

    Valid walking methods for the DOMNode.walk_children method.

    "},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier","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","text":"

    Bases: Exception

    Base exception class for errors relating to the DOM.

    "},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode","text":"
    DOMNode(*, 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.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = []\n

    A list of key bindings.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.BINDING_GROUP_TITLE","title":"BINDING_GROUP_TITLE class-attribute instance-attribute","text":"
    BINDING_GROUP_TITLE = None\n

    Title of widget used where bindings are displayed (such as in the key panel).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = set()\n

    Virtual DOM nodes, used to expose styles to line API widgets.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.DEFAULT_CLASSES","title":"DEFAULT_CLASSES class-attribute","text":"
    DEFAULT_CLASSES = ''\n

    Default classes argument if not supplied.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.DEFAULT_CSS","title":"DEFAULT_CSS class-attribute","text":"
    DEFAULT_CSS = ''\n

    Default TCSS.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.HELP","title":"HELP class-attribute","text":"
    HELP = None\n

    Optional help text shown in help panel (Markdown format).

    "},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
    SCOPED_CSS = True\n

    Should default css be limited to the widget type?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
    ancestors\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\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\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\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\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\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\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\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\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\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\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\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\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\n

    Is the node a modal?

    "},{"location":"api/dom_node/#textual.dom.DOMNode.is_on_screen","title":"is_on_screen property","text":"
    is_on_screen\n

    Check if the node was displayed in the last screen update.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
    name\n

    The name of the node.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
    parent\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\n

    A (frozen) set of all pseudo classes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
    rich_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\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\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\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\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\n

    The app's worker manager. Shortcut for self.app.workers.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.action_toggle","title":"action_toggle async","text":"
    action_toggle(attribute_name)\n

    Toggle an attribute on the node.

    Assumes the attribute is a bool.

    Parameters:

    Name Type Description Default str

    Name of the attribute.

    required"},{"location":"api/dom_node/#textual.dom.DOMNode.action_toggle(attribute_name)","title":"attribute_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class","text":"
    add_class(*class_names, update=True)\n

    Add class names to this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to add.

    () bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.add_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.add_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.automatic_refresh","title":"automatic_refresh","text":"
    automatic_refresh()\n

    Perform an automatic refresh.

    This method is called when you set the auto_refresh attribute. You could implement this method if you want to perform additional work during an automatic refresh.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_action","title":"check_action","text":"
    check_action(action, parameters)\n

    Check whether an action is enabled.

    Implement this method to add logic for dynamic actions / bindings.

    Parameters:

    Name Type Description Default str

    The name of an action.

    required tuple[object, ...]

    A tuple of any action parameters.

    required

    Returns:

    Type Description bool | None

    True if the action is enabled+visible, False if the action is disabled+hidden, None if the action is disabled+visible (grayed out in footer)

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_action(action)","title":"action","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_action(parameters)","title":"parameters","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character)\n

    Check if the widget may consume the given key.

    This should be implemented in widgets that handle Key events and stop propagation (such as Input and TextArea).

    Implementing this method will hide key bindings from the footer and key panel that would be consumed by the focused widget.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    required

    Returns:

    Type Description bool

    True if the widget may capture the key in its Key event handler, or False if it won't.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key(key)","title":"key","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.check_consume_key(character)","title":"character","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.data_bind","title":"data_bind","text":"
    data_bind(*reactives, **bind_vars)\n

    Bind reactive data so that changes to a reactive automatically change the reactive on another widget.

    Reactives may be given as positional arguments or keyword arguments. See the guide on data binding.

    Example
    def compose(self) -> ComposeResult:\n    yield WorldClock(\"Europe/London\").data_bind(WorldClockApp.time)\n    yield WorldClock(\"Europe/Paris\").data_bind(WorldClockApp.time)\n    yield WorldClock(\"Asia/Tokyo\").data_bind(WorldClockApp.time)\n

    Raises:

    Type Description ReactiveError

    If the data wasn't bound.

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles","text":"
    get_component_styles(*names)\n

    Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

    Parameters:

    Name Type Description Default str

    Names of the components.

    ()

    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_component_styles(names)","title":"names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes","text":"
    get_pseudo_classes()\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","text":"
    has_class(*class_names)\n

    Check if the Node has all the given class names.

    Parameters:

    Name Type Description Default 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_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class","text":"
    has_pseudo_class(class_name)\n

    Check the node has the given pseudo class.

    Parameters:

    Name Type Description Default str

    The pseudo class to check for.

    required

    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_class(class_name)","title":"class_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_classes","title":"has_pseudo_classes","text":"
    has_pseudo_classes(class_names)\n

    Check the node has all the given pseudo classes.

    Parameters:

    Name Type Description Default set[str]

    Set of class names to check for.

    required

    Returns:

    Type Description bool

    True if all pseudo class names are present.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_classes(class_names)","title":"class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.mutate_reactive","title":"mutate_reactive","text":"
    mutate_reactive(reactive)\n

    Force an update to a mutable reactive.

    Example
    self.reactive_name_list.append(\"Jessica\")\nself.mutate_reactive(MyClass.reactive_name_list)\n

    Textual will automatically detect when a reactive is set to a new value, but it is unable to detect if a value is mutated (such as updating a list, dict, or attribute of an object). If you do wish to use a collection or other mutable object in a reactive, then you can call this method after your reactive is updated. This will ensure that all the reactive superpowers work.

    Note

    This method will cause watchers to be called, even if the value hasn't changed.

    Parameters:

    Name Type Description Default Reactive[ReactiveType]

    A reactive property (use the class scope syntax, i.e. MyClass.my_reactive).

    required"},{"location":"api/dom_node/#textual.dom.DOMNode.mutate_reactive(reactive)","title":"reactive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\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","text":"
    query(selector: str | None = None) -> DOMQuery[Widget]\n
    query(selector: type[QueryType]) -> DOMQuery[QueryType]\n
    query(selector=None)\n

    Query the DOM for children that match a selector or widget type.

    Parameters:

    Name Type Description Default str | type[QueryType] | None

    A CSS selector, widget type, or None for all nodes.

    None

    Returns:

    Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_children","title":"query_children","text":"
    query_children(\n    selector: str | None = None,\n) -> DOMQuery[Widget]\n
    query_children(\n    selector: type[QueryType],\n) -> DOMQuery[QueryType]\n
    query_children(selector=None)\n

    Query the DOM for the immediate children that match a selector or widget type.

    Note that this will not return child widgets more than a single level deep. If you want to a query to potentially match all children in the widget tree, see query.

    Parameters:

    Name Type Description Default str | type[QueryType] | None

    A CSS selector, widget type, or None for all nodes.

    None

    Returns:

    Type Description DOMQuery[Widget] | DOMQuery[QueryType]

    A query object.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_children(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one","title":"query_exactly_one","text":"
    query_exactly_one(selector: str) -> Widget\n
    query_exactly_one(selector: type[QueryType]) -> QueryType\n
    query_exactly_one(\n    selector: str, expect_type: type[QueryType]\n) -> QueryType\n
    query_exactly_one(selector, expect_type=None)\n

    Get a widget from this widget's children that matches a selector or widget type.

    Note

    This method is similar to query_one. The only difference is that it will raise TooManyMatches if there is more than a single match.

    Parameters:

    Name Type Description Default str | type[QueryType]

    A selector or widget type.

    required type[QueryType] | None

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

    None

    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 (and exactly_one==True).

    Returns:

    Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_exactly_one(expect_type)","title":"expect_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one","text":"
    query_one(selector: str) -> Widget\n
    query_one(selector: type[QueryType]) -> QueryType\n
    query_one(\n    selector: str, expect_type: type[QueryType]\n) -> QueryType\n
    query_one(selector, expect_type=None)\n

    Get a widget from this widget's children that matches a selector or widget type.

    Parameters:

    Name Type Description Default str | type[QueryType]

    A selector or widget type.

    required type[QueryType] | None

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

    None

    Raises:

    Type Description WrongType

    If the wrong type was found.

    NoMatches

    If no node matches the query.

    Returns:

    Type Description QueryType | Widget

    A widget matching the selector.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.query_one(selector)","title":"selector","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.query_one(expect_type)","title":"expect_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.refresh_bindings","title":"refresh_bindings","text":"
    refresh_bindings()\n

    Call to prompt widgets such as the Footer to update the display of key bindings.

    See actions for how to use this method.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class","text":"
    remove_class(*class_names, update=True)\n

    Remove class names from this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to remove.

    () bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles","text":"
    reset_styles()\n

    Reset styles back to their initial state.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker","text":"
    run_worker(\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:

    Name Type Description Default WorkType[ResultType]

    A function, async function, or an awaitable object to run in a worker.

    required str | None

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' str

    A longer string to store longer information on the worker.

    '' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Start the worker immediately.

    True bool

    Cancel all workers in the same group.

    False bool

    Mark the worker as a thread worker.

    False

    Returns:

    Type Description Worker[ResultType]

    New Worker instance.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(work)","title":"work","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(name)","title":"name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(group)","title":"group","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(description)","title":"description","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(start)","title":"start","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(exclusive)","title":"exclusive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker(thread)","title":"thread","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class","text":"
    set_class(add, *class_names, update=True)\n

    Add or remove class(es) based on a condition.

    Parameters:

    Name Type Description Default bool

    Add the classes if True, otherwise remove them.

    required bool

    Also update styles.

    True

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_class(add)","title":"add","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_class(update)","title":"update","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes","text":"
    set_classes(classes)\n

    Replace all classes.

    Parameters:

    Name Type Description Default str | Iterable[str]

    A string containing space separated classes, or an iterable of class names.

    required

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes(classes)","title":"classes","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive","title":"set_reactive","text":"
    set_reactive(reactive, value)\n

    Sets a reactive value without invoking validators or watchers.

    Example
    self.set_reactive(App.dark_mode, True)\n

    Parameters:

    Name Type Description Default Reactive[ReactiveType]

    A reactive property (use the class scope syntax, i.e. MyClass.my_reactive).

    required ReactiveType

    New value of reactive.

    required

    Raises:

    Type Description AttributeError

    If the first argument is not a reactive.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive(reactive)","title":"reactive","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_reactive(value)","title":"value","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles","text":"
    set_styles(css=None, **update_styles)\n

    Set custom styles on this object.

    Parameters:

    Name Type Description Default str | None

    Styles in CSS format.

    None Any

    Keyword arguments map style names onto style values.

    {}

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles(css)","title":"css","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles(update_styles)","title":"update_styles","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children","title":"sort_children","text":"
    sort_children(*, key=None, reverse=False)\n

    Sort child widgets with an optional key function.

    If key is not provided then widgets will be sorted in the order they are constructed.

    Example
    # Sort widgets by name\nscreen.sort_children(key=lambda widget: widget.name or \"\")\n

    Parameters:

    Name Type Description Default Callable[[Widget], SupportsRichComparison] | None

    A callable which accepts a widget and returns something that can be sorted, or None to sort without a key function.

    None bool

    Sort in descending order.

    False"},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children(key)","title":"key","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.sort_children(reverse)","title":"reverse","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class","text":"
    toggle_class(*class_names)\n

    Toggle class names on this Node.

    Parameters:

    Name Type Description Default str

    CSS class names to toggle.

    ()

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class(*class_names)","title":"*class_names","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children","text":"
    walk_children(\n    filter_type: type[WalkType],\n    *,\n    with_self: bool = False,\n    method: WalkMethod = \"depth\",\n    reverse: bool = False\n) -> list[WalkType]\n
    walk_children(\n    *,\n    with_self: bool = False,\n    method: WalkMethod = \"depth\",\n    reverse: bool = False\n) -> list[DOMNode]\n
    walk_children(\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:

    Name Type Description Default type[WalkType] | None

    Filter only this type, or None for no filter.

    None bool

    Also yield self in addition to descendants.

    False WalkMethod

    One of \"depth\" or \"breadth\".

    'depth' bool

    Reverse the order (bottom up).

    False

    Returns:

    Type Description list[DOMNode] | list[WalkType]

    A list of nodes.

    "},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(filter_type)","title":"filter_type","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(with_self)","title":"with_self","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(method)","title":"method","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children(reverse)","title":"reverse","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch","text":"
    watch(obj, attribute_name, callback, init=True)\n

    Watches for modifications to reactive attributes on another object.

    Example
    def on_dark_change(old_value:bool, new_value:bool) -> None:\n    # Called when app.dark changes.\n    print(\"App.dark went from {old_value} to {new_value}\")\n\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n

    Parameters:

    Name Type Description Default DOMNode

    Object containing attribute to watch.

    required str

    Attribute to watch.

    required WatchCallbackType

    A callback to run when attribute changes.

    required bool

    Check watchers on first call.

    True"},{"location":"api/dom_node/#textual.dom.DOMNode.watch(obj)","title":"obj","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(attribute_name)","title":"attribute_name","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(callback)","title":"callback","text":""},{"location":"api/dom_node/#textual.dom.DOMNode.watch(init)","title":"init","text":""},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen","text":"

    Bases: DOMError

    Raised when the node has no associated screen.

    "},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers","text":"
    check_identifiers(description, *names)\n

    Validate identifier and raise an error if it fails.

    Parameters:

    Name Type Description Default str

    Description of where identifier is used for error message.

    required str

    Identifiers to check.

    ()"},{"location":"api/dom_node/#textual.dom.check_identifiers(description)","title":"description","text":""},{"location":"api/dom_node/#textual.dom.check_identifiers(*names)","title":"*names","text":""},{"location":"api/errors/","title":"textual.errors","text":"

    General exception classes.

    "},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers","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","text":"

    Bases: TextualError

    Specified widget was not found.

    "},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError","text":"

    Bases: TextualError

    An object could not be rendered.

    "},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError","text":"

    Bases: Exception

    Base class for Textual errors.

    "},{"location":"api/events/","title":"textual.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","text":"
    AppBlur()\n

    Bases: Event

    Sent when the app loses focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusOut, or when running via textual-web.

    "},{"location":"api/events/#textual.events.AppFocus","title":"AppFocus","text":"
    AppFocus()\n

    Bases: Event

    Sent when the app has focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusIn, or when running via textual-web.

    "},{"location":"api/events/#textual.events.Blur","title":"Blur","text":"
    Blur()\n

    Bases: Event

    Sent when a widget is blurred (un-focussed).

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Callback","title":"Callback","text":"
    Callback(callback)\n

    Bases: Event

    Sent by Textual to invoke a callback (see call_next and call_later).

    "},{"location":"api/events/#textual.events.Click","title":"Click","text":"
    Click(\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: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Compose","title":"Compose","text":"
    Compose()\n

    Bases: Event

    Sent to a widget to request it to compose and mount children.

    This event is used internally by Textual. You won't typically need to explicitly handle it,

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.CursorPosition","title":"CursorPosition dataclass","text":"
    CursorPosition(x, y)\n

    Bases: Event

    Internal event used to retrieve the terminal's cursor position.

    "},{"location":"api/events/#textual.events.DeliveryComplete","title":"DeliveryComplete dataclass","text":"
    DeliveryComplete(key, path=None, name=None)\n

    Bases: Event

    Sent to App when a file has been delivered.

    "},{"location":"api/events/#textual.events.DeliveryComplete.key","title":"key instance-attribute","text":"
    key\n

    The delivery key associated with the delivery.

    This is the same key that was returned by App.deliver_text/App.deliver_binary.

    "},{"location":"api/events/#textual.events.DeliveryComplete.name","title":"name class-attribute instance-attribute","text":"
    name = None\n

    Optional name returned to the app to identify the download.

    "},{"location":"api/events/#textual.events.DeliveryComplete.path","title":"path class-attribute instance-attribute","text":"
    path = None\n

    The path where the file was saved, or None if the path is not available, for example if the file was delivered via web browser.

    "},{"location":"api/events/#textual.events.DeliveryFailed","title":"DeliveryFailed dataclass","text":"
    DeliveryFailed(key, exception, name=None)\n

    Bases: Event

    Sent to App when a file delivery fails.

    "},{"location":"api/events/#textual.events.DeliveryFailed.exception","title":"exception instance-attribute","text":"
    exception\n

    The exception that was raised during the delivery.

    "},{"location":"api/events/#textual.events.DeliveryFailed.key","title":"key instance-attribute","text":"
    key\n

    The delivery key associated with the delivery.

    "},{"location":"api/events/#textual.events.DeliveryFailed.name","title":"name class-attribute instance-attribute","text":"
    name = None\n

    Optional name returned to the app to identify the download.

    "},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur dataclass","text":"
    DescendantBlur(widget)\n

    Bases: Event

    Sent when a child widget is blurred.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
    control\n

    The widget that was blurred (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was blurred.

    "},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus dataclass","text":"
    DescendantFocus(widget)\n

    Bases: Event

    Sent when a child widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
    control\n

    The widget that was focused (alias of widget).

    "},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was focused.

    "},{"location":"api/events/#textual.events.Enter","title":"Enter","text":"
    Enter(node)\n

    Bases: Event

    Sent when the mouse is moved over a widget.

    Note that this event bubbles, so a widget may receive this event when the mouse moves over a child widget. Check the node attribute for the widget directly under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Enter.node","title":"node instance-attribute","text":"
    node = node\n

    The node directly under the mouse.

    "},{"location":"api/events/#textual.events.Event","title":"Event","text":"
    Event()\n

    Bases: Message

    The base class for all events.

    "},{"location":"api/events/#textual.events.Focus","title":"Focus","text":"
    Focus()\n

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Hide","title":"Hide","text":"
    Hide()\n

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    Sent when any of the following conditions apply:

    • The widget is removed from the DOM.
    • The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
    • The widget has its display attribute set to False.
    • The widget's display style is set to \"none\".
    "},{"location":"api/events/#textual.events.Idle","title":"Idle","text":"
    Idle()\n

    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","text":"
    InputEvent()\n

    Bases: Event

    Base class for input events.

    "},{"location":"api/events/#textual.events.Key","title":"Key","text":"
    Key(key, character)\n

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    The key that was pressed.

    required str | None

    A printable character or None if it is not printable.

    required"},{"location":"api/events/#textual.events.Key(key)","title":"key","text":""},{"location":"api/events/#textual.events.Key(character)","title":"character","text":""},{"location":"api/events/#textual.events.Key.aliases","title":"aliases instance-attribute","text":"
    aliases = _get_key_aliases(key)\n

    The aliases for the key, including the key itself.

    "},{"location":"api/events/#textual.events.Key.character","title":"character instance-attribute","text":"
    character = (\n    key\n    if len(key) == 1\n    else None if character is None else character\n)\n

    A printable character or None if it is not printable.

    "},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable\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.key","title":"key instance-attribute","text":"
    key = key\n

    The key that was pressed.

    "},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
    name\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\n

    The corresponding name for every alias in aliases list.

    "},{"location":"api/events/#textual.events.Leave","title":"Leave","text":"
    Leave(node)\n

    Bases: Event

    Sent when the mouse is moved away from a widget, or if a widget is programmatically disabled while hovered.

    Note that this widget bubbles, so a widget may receive Leave events for any child widgets. Check the node parameter for the original widget that was previously under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Leave.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was previously directly under the mouse.

    "},{"location":"api/events/#textual.events.Load","title":"Load","text":"
    Load()\n

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Mount","title":"Mount","text":"
    Mount()\n

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture","text":"
    MouseCapture(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:

    Name Type Description Default Offset

    The position of the mouse when captured.

    required"},{"location":"api/events/#textual.events.MouseCapture(mouse_position)","title":"mouse_position","text":""},{"location":"api/events/#textual.events.MouseCapture.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when captured.

    "},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown","text":"
    MouseDown(\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: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent","text":"
    MouseEvent(\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:

    Name Type Description Default int

    The relative x coordinate.

    required int

    The relative y coordinate.

    required int

    Change in x since the last message.

    required int

    Change in y since the last message.

    required int

    Indexed of the pressed button.

    required bool

    True if the shift key is pressed.

    required bool

    True if the meta key is pressed.

    required bool

    True if the ctrl key is pressed.

    required int | None

    The absolute x coordinate.

    None int | None

    The absolute y coordinate.

    None Style | None

    The Rich Style under the mouse cursor.

    None"},{"location":"api/events/#textual.events.MouseEvent(x)","title":"x","text":""},{"location":"api/events/#textual.events.MouseEvent(y)","title":"y","text":""},{"location":"api/events/#textual.events.MouseEvent(delta_x)","title":"delta_x","text":""},{"location":"api/events/#textual.events.MouseEvent(delta_y)","title":"delta_y","text":""},{"location":"api/events/#textual.events.MouseEvent(button)","title":"button","text":""},{"location":"api/events/#textual.events.MouseEvent(shift)","title":"shift","text":""},{"location":"api/events/#textual.events.MouseEvent(meta)","title":"meta","text":""},{"location":"api/events/#textual.events.MouseEvent(ctrl)","title":"ctrl","text":""},{"location":"api/events/#textual.events.MouseEvent(screen_x)","title":"screen_x","text":""},{"location":"api/events/#textual.events.MouseEvent(screen_y)","title":"screen_y","text":""},{"location":"api/events/#textual.events.MouseEvent(style)","title":"style","text":""},{"location":"api/events/#textual.events.MouseEvent.button","title":"button instance-attribute","text":"
    button = button\n

    Indexed of the pressed button.

    "},{"location":"api/events/#textual.events.MouseEvent.ctrl","title":"ctrl instance-attribute","text":"
    ctrl = ctrl\n

    True if the ctrl key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
    delta\n

    Mouse coordinate delta (change since last event).

    "},{"location":"api/events/#textual.events.MouseEvent.delta_x","title":"delta_x instance-attribute","text":"
    delta_x = delta_x\n

    Change in x since the last message.

    "},{"location":"api/events/#textual.events.MouseEvent.delta_y","title":"delta_y instance-attribute","text":"
    delta_y = delta_y\n

    Change in y since the last message.

    "},{"location":"api/events/#textual.events.MouseEvent.meta","title":"meta instance-attribute","text":"
    meta = meta\n

    True if the meta key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
    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\n

    Mouse coordinate relative to the screen.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_x","title":"screen_x instance-attribute","text":"
    screen_x = x if screen_x is None else screen_x\n

    The absolute x coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.screen_y","title":"screen_y instance-attribute","text":"
    screen_y = y if screen_y is None else screen_y\n

    The absolute y coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.shift","title":"shift instance-attribute","text":"
    shift = shift\n

    True if the shift key is pressed.

    "},{"location":"api/events/#textual.events.MouseEvent.style","title":"style property writable","text":"
    style\n

    The (Rich) Style under the cursor.

    "},{"location":"api/events/#textual.events.MouseEvent.x","title":"x instance-attribute","text":"
    x = x\n

    The relative x coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.y","title":"y instance-attribute","text":"
    y = y\n

    The relative y coordinate.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset","text":"
    get_content_offset(widget)\n

    Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

    Parameters:

    Name Type Description Default Widget

    Widget receiving the event.

    required

    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(widget)","title":"widget","text":""},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture","text":"
    get_content_offset_capture(widget)\n

    Get offset from a widget's content area.

    This method works even if the offset is outside the widget content region.

    Parameters:

    Name Type Description Default Widget

    Widget receiving the event.

    required

    Returns:

    Type Description Offset

    An offset where the origin is at the top left of the content area.

    "},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture(widget)","title":"widget","text":""},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove","text":"
    MouseMove(\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: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease","text":"
    MouseRelease(mouse_position)\n

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Offset

    The position of the mouse when released.

    required"},{"location":"api/events/#textual.events.MouseRelease(mouse_position)","title":"mouse_position","text":""},{"location":"api/events/#textual.events.MouseRelease.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when released.

    "},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown","text":"
    MouseScrollDown(\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: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp","text":"
    MouseScrollUp(\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: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp","text":"
    MouseUp(\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: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Paste","title":"Paste","text":"
    Paste(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:

    Name Type Description Default str

    The text that has been pasted.

    required"},{"location":"api/events/#textual.events.Paste(text)","title":"text","text":""},{"location":"api/events/#textual.events.Paste.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was pasted.

    "},{"location":"api/events/#textual.events.Print","title":"Print","text":"
    Print(text, stderr=False)\n

    Bases: Event

    Sent to a widget that is capturing print.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    Text that was printed.

    required bool

    True if the print was to stderr, or False for stdout.

    False Note

    Python's print output can be captured with App.begin_capture_print.

    "},{"location":"api/events/#textual.events.Print(text)","title":"text","text":""},{"location":"api/events/#textual.events.Print(stderr)","title":"stderr","text":""},{"location":"api/events/#textual.events.Print.stderr","title":"stderr instance-attribute","text":"
    stderr = stderr\n

    True if the print was to stderr, or False for stdout.

    "},{"location":"api/events/#textual.events.Print.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was printed.

    "},{"location":"api/events/#textual.events.Ready","title":"Ready","text":"
    Ready()\n

    Bases: Event

    Sent to the App when the DOM is ready and the first frame has been displayed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Resize","title":"Resize","text":"
    Resize(size, virtual_size, container_size=None)\n

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Size

    The new size of the Widget.

    required Size

    The virtual size (scrollable size) of the Widget.

    required Size | None

    The size of the Widget's container widget.

    None"},{"location":"api/events/#textual.events.Resize(size)","title":"size","text":""},{"location":"api/events/#textual.events.Resize(virtual_size)","title":"virtual_size","text":""},{"location":"api/events/#textual.events.Resize(container_size)","title":"container_size","text":""},{"location":"api/events/#textual.events.Resize.container_size","title":"container_size instance-attribute","text":"
    container_size = (\n    size if container_size is None else container_size\n)\n

    The size of the Widget's container widget.

    "},{"location":"api/events/#textual.events.Resize.size","title":"size instance-attribute","text":"
    size = size\n

    The new size of the Widget.

    "},{"location":"api/events/#textual.events.Resize.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size = virtual_size\n

    The virtual size (scrollable size) of the Widget.

    "},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume","text":"
    ScreenResume()\n

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend","text":"
    ScreenSuspend()\n

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Show","title":"Show","text":"
    Show()\n

    Bases: Event

    Sent when a widget is first displayed.

    • Bubbles
    • Verbose
    "},{"location":"api/events/#textual.events.Timer","title":"Timer","text":"
    Timer(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","text":"
    Unmount()\n

    Bases: Event

    Sent when a widget is unmounted and may no longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"api/filter/","title":"textual.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","text":"
    ANSIToTruecolor(terminal_theme, enabled=True)\n

    Bases: LineFilter

    Convert ANSI colors to their truecolor equivalents.

    Parameters:

    Name Type Description Default TerminalTheme

    A rich terminal theme.

    required"},{"location":"api/filter/#textual.filter.ANSIToTruecolor(terminal_theme)","title":"terminal_theme","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
    truecolor_style(style)\n

    Replace system colors with truecolor equivalent.

    Parameters:

    Name Type Description Default Style

    Style to apply truecolor filter to.

    required

    Returns:

    Type Description Style

    New style.

    "},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style(style)","title":"style","text":""},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter","text":"
    DimFilter(dim_factor=0.5)\n

    Bases: LineFilter

    Replace dim attributes with modified colors.

    Parameters:

    Name Type Description Default float

    The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

    0.5"},{"location":"api/filter/#textual.filter.DimFilter(dim_factor)","title":"dim_factor","text":""},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.DimFilter.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.DimFilter.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter","text":"
    LineFilter(enabled=True)\n

    Bases: ABC

    Base class for a line filter.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.LineFilter.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.LineFilter.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome","text":"
    Monochrome(enabled=True)\n

    Bases: LineFilter

    Convert all colors to monochrome.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.Monochrome.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.Monochrome.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.NoColor","title":"NoColor","text":"
    NoColor(enabled=True)\n

    Bases: LineFilter

    Remove all color information from segments.

    "},{"location":"api/filter/#textual.filter.NoColor.apply","title":"apply","text":"
    apply(segments, background)\n

    Transform a list of segments.

    Parameters:

    Name Type Description Default list[Segment]

    A list of segments.

    required Color

    The background color.

    required

    Returns:

    Type Description list[Segment]

    A new list of segments.

    "},{"location":"api/filter/#textual.filter.NoColor.apply(segments)","title":"segments","text":""},{"location":"api/filter/#textual.filter.NoColor.apply(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
    dim_color(background, color, factor)\n

    Dim a color by blending towards the background

    Parameters:

    Name Type Description Default Color

    background color.

    required Color

    Foreground color.

    required float

    Blend factor

    required

    Returns:

    Type Description Color

    New dimmer color.

    "},{"location":"api/filter/#textual.filter.dim_color(background)","title":"background","text":""},{"location":"api/filter/#textual.filter.dim_color(color)","title":"color","text":""},{"location":"api/filter/#textual.filter.dim_color(factor)","title":"factor","text":""},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
    dim_style(style, background, factor)\n

    Replace dim attribute with a dim color.

    Parameters:

    Name Type Description Default Style

    Style to dim.

    required float

    Blend factor.

    required

    Returns:

    Type Description Style

    New dimmed style.

    "},{"location":"api/filter/#textual.filter.dim_style(style)","title":"style","text":""},{"location":"api/filter/#textual.filter.dim_style(factor)","title":"factor","text":""},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
    monochrome_style(style)\n

    Convert colors in a style to monochrome.

    Parameters:

    Name Type Description Default Style

    A Rich Style.

    required

    Returns:

    Type Description Style

    A new Rich style.

    "},{"location":"api/filter/#textual.filter.monochrome_style(style)","title":"style","text":""},{"location":"api/fuzzy_matcher/","title":"textual.fuzzy","text":"

    Fuzzy matcher.

    This class is used by the command palette to match search terms.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher","text":"
    Matcher(query, *, match_style=None, case_sensitive=False)\n

    A fuzzy matcher.

    Parameters:

    Name Type Description Default str

    A query as typed in by the user.

    required Style | None

    The style to use to highlight matched portions of a string.

    None bool

    Should matching be case sensitive?

    False"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(query)","title":"query","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(match_style)","title":"match_style","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
    case_sensitive\n

    Is this matcher case sensitive?

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
    match_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\n

    The query string to look for.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
    query_pattern\n

    The regular expression pattern built from the query.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight","text":"
    highlight(candidate)\n

    Highlight the candidate with the fuzzy match.

    Parameters:

    Name Type Description Default str

    The candidate string to match against the query.

    required

    Returns:

    Type Description Text

    A [rich.text.Text][Text] object with highlighted matches.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight(candidate)","title":"candidate","text":""},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match","text":"
    match(candidate)\n

    Match the candidate against the query.

    Parameters:

    Name Type Description Default str

    Candidate string to match against the query.

    required

    Returns:

    Type Description float

    Strength of the match from 0 to 1.

    "},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match(candidate)","title":"candidate","text":""},{"location":"api/geometry/","title":"textual.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 = 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 = 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 = 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 = Spacing(0, 0, 0, 0)\n

    A Spacing constant for no space.

    "},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
    SpacingDimensions = 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","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\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\n

    Is the offset at (0, 0)?

    "},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x class-attribute instance-attribute","text":"
    x = 0\n

    Offset in the x-axis (horizontal)

    "},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y class-attribute instance-attribute","text":"
    y = 0\n

    Offset in the y-axis (vertical)

    "},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend","text":"
    blend(destination, factor)\n

    Calculate a new offset on a line between this offset and a destination offset.

    Parameters:

    Name Type Description Default Offset

    Point where factor would be 1.0.

    required float

    A value between 0 and 1.0.

    required

    Returns:

    Type Description Offset

    A new point on a line between self and destination.

    "},{"location":"api/geometry/#textual.geometry.Offset.blend(destination)","title":"destination","text":""},{"location":"api/geometry/#textual.geometry.Offset.blend(factor)","title":"factor","text":""},{"location":"api/geometry/#textual.geometry.Offset.clamp","title":"clamp","text":"
    clamp(width, height)\n

    Clamp the offset to fit within a rectangle of width x height.

    Parameters:

    Name Type Description Default int

    Width to clamp.

    required int

    Height to clamp.

    required

    Returns:

    Type Description Offset

    A new offset.

    "},{"location":"api/geometry/#textual.geometry.Offset.clamp(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Offset.clamp(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to","text":"
    get_distance_to(other)\n

    Get the distance to another offset.

    Parameters:

    Name Type Description Default Offset

    An offset.

    required

    Returns:

    Type Description float

    Distance to other offset.

    "},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region","title":"Region","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\n

    The area under the region.

    "},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
    bottom\n

    Maximum Y value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
    bottom_left\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\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\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\n

    A range object for X coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
    column_span\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\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 = 0\n

    The height of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
    line_range\n

    A range object for Y coordinates.

    "},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
    line_span\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\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\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\n

    Maximum X value (non inclusive).

    "},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
    size\n

    Get the size of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
    top_right\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 = 0\n

    The width of the region.

    "},{"location":"api/geometry/#textual.geometry.Region.x","title":"x class-attribute instance-attribute","text":"
    x = 0\n

    Offset in the x-axis (horizontal).

    "},{"location":"api/geometry/#textual.geometry.Region.y","title":"y class-attribute instance-attribute","text":"
    y = 0\n

    Offset in the y-axis (vertical).

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset","text":"
    at_offset(offset)\n

    Get a new Region with the same size at a given offset.

    Parameters:

    Name Type Description Default tuple[int, int]

    An offset.

    required

    Returns:

    Type Description Region

    New Region with adjusted offset.

    "},{"location":"api/geometry/#textual.geometry.Region.at_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip","text":"
    clip(width, height)\n

    Clip this region to fit within width, height.

    Parameters:

    Name Type Description Default int

    Width of bounds.

    required int

    Height of bounds.

    required

    Returns:

    Type Description Region

    Clipped region.

    "},{"location":"api/geometry/#textual.geometry.Region.clip(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Region.clip(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Region.constrain","title":"constrain","text":"
    constrain(constrain_x, constrain_y, margin, container)\n

    Constrain a region to fit within a container, using different methods per axis.

    Parameters:

    Name Type Description Default Literal['none', 'inside', 'inflect']

    Constrain method for the X-axis.

    required Literal['none', 'inside', 'inflect']

    Constrain method for the Y-axis.

    required Spacing

    Margin to maintain around region.

    required Region

    Container to constrain to.

    required

    Returns:

    Type Description Region

    New widget, that fits inside the container (if possible).

    "},{"location":"api/geometry/#textual.geometry.Region.constrain(constrain_x)","title":"constrain_x","text":""},{"location":"api/geometry/#textual.geometry.Region.constrain(constrain_y)","title":"constrain_y","text":""},{"location":"api/geometry/#textual.geometry.Region.constrain(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.constrain(container)","title":"container","text":""},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains","text":"
    contains(x, y)\n

    Check if a point is in the region.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains(x)","title":"x","text":""},{"location":"api/geometry/#textual.geometry.Region.contains(y)","title":"y","text":""},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point","text":"
    contains_point(point)\n

    Check if a point is in the region.

    Parameters:

    Name Type Description Default tuple[int, int]

    A tuple of x and y coordinates.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_point(point)","title":"point","text":""},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
    contains_region(other)\n

    Check if a region is entirely contained within this region.

    Parameters:

    Name Type Description Default Region

    A region.

    required

    Returns:

    Type Description bool

    True if the other region fits perfectly within this region.

    "},{"location":"api/geometry/#textual.geometry.Region.contains_region(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size","text":"
    crop_size(size)\n

    Get a region with the same offset, with a size no larger than size.

    Parameters:

    Name Type Description Default tuple[int, int]

    Maximum width and height (WIDTH, HEIGHT).

    required

    Returns:

    Type Description Region

    New region that could fit within size.

    "},{"location":"api/geometry/#textual.geometry.Region.crop_size(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand","text":"
    expand(size)\n

    Increase the size of the region by adding a border.

    Parameters:

    Name Type Description Default tuple[int, int]

    Additional width and height.

    required

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.expand(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
    from_corners(x1, y1, x2, y2)\n

    Construct a Region form the top left and bottom right corners.

    Parameters:

    Name Type Description Default int

    Top left x.

    required int

    Top left y.

    required int

    Bottom right x.

    required int

    Bottom right y.

    required

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.from_corners(x1)","title":"x1","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(y1)","title":"y1","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(x2)","title":"x2","text":""},{"location":"api/geometry/#textual.geometry.Region.from_corners(y2)","title":"y2","text":""},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
    from_offset(offset, size)\n

    Create a region from offset and size.

    Parameters:

    Name Type Description Default tuple[int, int]

    Offset (top left point).

    required tuple[int, int]

    Dimensions of region.

    required

    Returns:

    Type Description Region

    A region instance.

    "},{"location":"api/geometry/#textual.geometry.Region.from_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.from_offset(size)","title":"size","text":""},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
    from_union(regions)\n

    Create a Region from the union of other regions.

    Parameters:

    Name Type Description Default Collection[Region]

    One or more regions.

    required

    Returns:

    Type Description Region

    A Region that encloses all other regions.

    "},{"location":"api/geometry/#textual.geometry.Region.from_union(regions)","title":"regions","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
    get_scroll_to_visible(window_region, region, *, top=False)\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:

    Name Type Description Default Region

    The window region.

    required Region

    The region to move inside the window.

    required bool

    Get offset to top of window.

    False

    Returns:

    Type Description Offset

    An offset required to add to region to move it inside window_region.

    "},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(window_region)","title":"window_region","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible(top)","title":"top","text":""},{"location":"api/geometry/#textual.geometry.Region.get_spacing_between","title":"get_spacing_between","text":"
    get_spacing_between(region)\n

    Get spacing between two regions.

    Parameters:

    Name Type Description Default Region

    Another region.

    required

    Returns:

    Type Description Spacing

    Spacing that if subtracted from self produces region.

    "},{"location":"api/geometry/#textual.geometry.Region.get_spacing_between(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
    grow(margin)\n

    Grow a region by adding spacing.

    Parameters:

    Name Type Description Default tuple[int, int, int, int]

    Grow space by (<top>, <right>, <bottom>, <left>).

    required

    Returns:

    Type Description Region

    New region.

    "},{"location":"api/geometry/#textual.geometry.Region.grow(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect","text":"
    inflect(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.

    If a margin is provided, it will add space between the resulting region.

    Note that if margin is specified it overlaps, so the space will be the maximum of two edges, and not the total.

    \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 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                \u2502          \u2502\n                \u2502  Result  \u2502\n                \u2502          \u2502\n                \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n

    Parameters:

    Name Type Description Default int

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    +1 int

    +1 to inflect in the positive direction, -1 to inflect in the negative direction.

    +1 Spacing | None

    Additional margin.

    None

    Returns:

    Type Description Region

    A new region.

    "},{"location":"api/geometry/#textual.geometry.Region.inflect(x_axis)","title":"x_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect(y_axis)","title":"y_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.inflect(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
    intersection(region)\n

    Get the overlapping portion of the two regions.

    Parameters:

    Name Type Description Default Region

    A region that overlaps this region.

    required

    Returns:

    Type Description Region

    A new region that covers when the two regions overlap.

    "},{"location":"api/geometry/#textual.geometry.Region.intersection(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
    overlaps(other)\n

    Check if another region overlaps this region.

    Parameters:

    Name Type Description Default Region

    A Region.

    required

    Returns:

    Type Description bool

    True if other region shares any cells with this region.

    "},{"location":"api/geometry/#textual.geometry.Region.overlaps(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
    shrink(margin)\n

    Shrink a region by subtracting spacing.

    Parameters:

    Name Type Description Default tuple[int, int, int, int]

    Shrink space by (<top>, <right>, <bottom>, <left>).

    required

    Returns:

    Type Description Region

    The new, smaller region.

    "},{"location":"api/geometry/#textual.geometry.Region.shrink(margin)","title":"margin","text":""},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
    split(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:

    Name Type Description Default int

    Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

    required int

    Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

    required

    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(cut_x)","title":"cut_x","text":""},{"location":"api/geometry/#textual.geometry.Region.split(cut_y)","title":"cut_y","text":""},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
    split_horizontal(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:

    Name Type Description Default int

    An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

    required

    Returns:

    Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_horizontal(cut)","title":"cut","text":""},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
    split_vertical(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:

    Name Type Description Default int

    An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

    required

    Returns:

    Type Description tuple[Region, Region]

    Two regions, which add up to the original (self).

    "},{"location":"api/geometry/#textual.geometry.Region.split_vertical(cut)","title":"cut","text":""},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
    translate(offset)\n

    Move the offset of the Region.

    Parameters:

    Name Type Description Default tuple[int, int]

    Offset to add to region.

    required

    Returns:

    Type Description Region

    A new region shifted by (x, y).

    "},{"location":"api/geometry/#textual.geometry.Region.translate(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside","text":"
    translate_inside(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:

    Name Type Description Default Region

    A container region.

    required bool

    Allow translation of X axis.

    True bool

    Allow translation of Y axis.

    True

    Returns:

    Type Description Region

    A new region with same dimensions that fits with inside container.

    "},{"location":"api/geometry/#textual.geometry.Region.translate_inside(container)","title":"container","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside(x_axis)","title":"x_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.translate_inside(y_axis)","title":"y_axis","text":""},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
    union(region)\n

    Get the smallest region that contains both regions.

    Parameters:

    Name Type Description Default Region

    Another region.

    required

    Returns:

    Type Description Region

    An optimally sized region to cover both regions.

    "},{"location":"api/geometry/#textual.geometry.Region.union(region)","title":"region","text":""},{"location":"api/geometry/#textual.geometry.Size","title":"Size","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\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 = 0\n

    The height in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
    line_range\n

    A range object that covers values between 0 and height.

    "},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
    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 = 0\n

    The width in cells.

    "},{"location":"api/geometry/#textual.geometry.Size.clamp_offset","title":"clamp_offset","text":"
    clamp_offset(offset)\n

    Clamp an offset to fit within the width x height.

    Parameters:

    Name Type Description Default Offset

    An offset.

    required

    Returns:

    Type Description Offset

    A new offset that will fit inside the dimensions defined in the Size.

    "},{"location":"api/geometry/#textual.geometry.Size.clamp_offset(offset)","title":"offset","text":""},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains","text":"
    contains(x, y)\n

    Check if a point is in area defined by the size.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains(x)","title":"x","text":""},{"location":"api/geometry/#textual.geometry.Size.contains(y)","title":"y","text":""},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point","text":"
    contains_point(point)\n

    Check if a point is in the area defined by the size.

    Parameters:

    Name Type Description Default tuple[int, int]

    A tuple of x and y coordinates.

    required

    Returns:

    Type Description bool

    True if the point is within the region.

    "},{"location":"api/geometry/#textual.geometry.Size.contains_point(point)","title":"point","text":""},{"location":"api/geometry/#textual.geometry.Size.with_height","title":"with_height","text":"
    with_height(height)\n

    Get a new Size with just the height changed.

    Parameters:

    Name Type Description Default int

    New height.

    required

    Returns:

    Type Description Size

    New Size instance.

    "},{"location":"api/geometry/#textual.geometry.Size.with_height(height)","title":"height","text":""},{"location":"api/geometry/#textual.geometry.Size.with_width","title":"with_width","text":"
    with_width(width)\n

    Get a new Size with just the width changed.

    Parameters:

    Name Type Description Default int

    New width.

    required

    Returns:

    Type Description Size

    New Size instance.

    "},{"location":"api/geometry/#textual.geometry.Size.with_width(width)","title":"width","text":""},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing","text":"

    Bases: NamedTuple

    Stores spacing around a widget, 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 = 0\n

    Space from the bottom of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
    bottom_right\n

    A pair of integers for the right, and bottom space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
    css\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\n

    Total space in the y axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left class-attribute instance-attribute","text":"
    left = 0\n

    Space from the left of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.max_height","title":"max_height property","text":"
    max_height\n

    The space between regions in the Y direction if margins overlap, i.e. max(self.top, self.bottom).

    "},{"location":"api/geometry/#textual.geometry.Spacing.max_width","title":"max_width property","text":"
    max_width\n

    The space between regions in the X direction if margins overlap, i.e. max(self.left, self.right).

    "},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right class-attribute instance-attribute","text":"
    right = 0\n

    Space from the right of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top class-attribute instance-attribute","text":"
    top = 0\n

    Space from the top of a region.

    "},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
    top_left\n

    A pair of integers for the left, and top space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
    totals\n

    A pair of integers for the total horizontal and vertical space.

    "},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
    width\n

    Total space in the x axis.

    "},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
    all(amount)\n

    Construct a Spacing with a given amount of spacing on all edges.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to all edges.

    required

    Returns:

    Type Description Spacing

    Spacing(amount, amount, amount, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.all(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum","text":"
    grow_maximum(other)\n

    Grow spacing with a maximum.

    Parameters:

    Name Type Description Default Spacing

    Spacing object.

    required

    Returns:

    Type Description Spacing

    New spacing where the values are maximum of the two values.

    "},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum(other)","title":"other","text":""},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
    horizontal(amount)\n

    Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to horizontal edges.

    required

    Returns:

    Type Description Spacing

    Spacing(0, amount, 0, amount)

    "},{"location":"api/geometry/#textual.geometry.Spacing.horizontal(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
    unpack(pad)\n

    Unpack padding specified in CSS style.

    Parameters:

    Name Type Description Default SpacingDimensions

    An integer, or tuple of 1, 2, or 4 integers.

    required

    Raises:

    Type Description ValueError

    If pad is an invalid value.

    Returns:

    Type Description Spacing

    New Spacing object.

    "},{"location":"api/geometry/#textual.geometry.Spacing.unpack(pad)","title":"pad","text":""},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
    vertical(amount)\n

    Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

    Parameters:

    Name Type Description Default int

    The magnitude of spacing to apply to vertical edges.

    required

    Returns:

    Type Description Spacing

    Spacing(amount, 0, amount, 0)

    "},{"location":"api/geometry/#textual.geometry.Spacing.vertical(amount)","title":"amount","text":""},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp","text":"
    clamp(value, minimum, maximum)\n

    Restrict a value to a given range.

    If value is less than the minimum, return the minimum. If value is greater than the maximum, return the maximum. Otherwise, return value.

    The minimum and maximum arguments values may be given in reverse order.

    Parameters:

    Name Type Description Default T

    A value.

    required T

    Minimum value.

    required T

    Maximum value.

    required

    Returns:

    Type Description T

    New value that is not less than the minimum or greater than the maximum.

    "},{"location":"api/geometry/#textual.geometry.clamp(value)","title":"value","text":""},{"location":"api/geometry/#textual.geometry.clamp(minimum)","title":"minimum","text":""},{"location":"api/geometry/#textual.geometry.clamp(maximum)","title":"maximum","text":""},{"location":"api/lazy/","title":"textual.lazy","text":"

    Tools for lazy loading widgets.

    "},{"location":"api/lazy/#textual.lazy.Lazy","title":"Lazy","text":"
    Lazy(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:

    Name Type Description Default Widget

    A widget that should be mounted after a refresh.

    required"},{"location":"api/lazy/#textual.lazy.Lazy(widget)","title":"widget","text":""},{"location":"api/logger/","title":"textual","text":"

    The root Textual module.

    Exposes some commonly used symbols.

    "},{"location":"api/logger/#textual.log","title":"log module-attribute","text":"
    log = Logger(None)\n

    Global logger that logs to the currently active app.

    Example
    from textual import log\nlog(locals())\n
    "},{"location":"api/logger/#textual.Logger","title":"Logger","text":"
    Logger(log_callable, group=INFO, verbosity=NORMAL)\n

    A logger class that logs to the Textual console.

    "},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
    debug\n

    Logs debug messages.

    "},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
    error\n

    Logs errors.

    "},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
    event\n

    Logs events.

    "},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
    info\n

    Logs information.

    "},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
    logging\n

    Logs from stdlib logging module.

    "},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
    system\n

    Logs system information.

    "},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
    verbose\n

    A verbose logger.

    "},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
    warning\n

    Logs warnings.

    "},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
    worker\n

    Logs worker information.

    "},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity","text":"
    verbosity(verbose)\n

    Get a new logger with selective verbosity.

    Parameters:

    Name Type Description Default bool

    True to use HIGH verbosity, otherwise NORMAL.

    required

    Returns:

    Type Description Logger

    New logger.

    "},{"location":"api/logger/#textual.Logger.verbosity(verbose)","title":"verbose","text":""},{"location":"api/logger/#textual.LoggerError","title":"LoggerError","text":"

    Bases: Exception

    Raised when the logger failed.

    "},{"location":"api/logger/#textual.on","title":"on","text":"
    on(message_type, selector=None, **kwargs)\n

    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\", pane=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n

    Parameters:

    Name Type Description Default type[Message]

    The message type (i.e. the class).

    required str | None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    None str

    Additional selectors for other attributes of the message.

    {}"},{"location":"api/logger/#textual.on(message_type)","title":"message_type","text":""},{"location":"api/logger/#textual.on(selector)","title":"selector","text":""},{"location":"api/logger/#textual.on(**kwargs)","title":"**kwargs","text":""},{"location":"api/logger/#textual.work","title":"work","text":"
    work(\n    method: Callable[\n        FactoryParamSpec, Coroutine[None, None, ReturnType]\n    ],\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Callable[FactoryParamSpec, \"Worker[ReturnType]\"]\n
    work(\n    method: Callable[FactoryParamSpec, ReturnType],\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Callable[FactoryParamSpec, \"Worker[ReturnType]\"]\n
    work(\n    *,\n    name: str = \"\",\n    group: str = \"default\",\n    exit_on_error: bool = True,\n    exclusive: bool = False,\n    description: str | None = None,\n    thread: bool = False\n) -> Decorator[..., ReturnType]\n
    work(\n    method=None,\n    *,\n    name=\"\",\n    group=\"default\",\n    exit_on_error=True,\n    exclusive=False,\n    description=None,\n    thread=False\n)\n

    A decorator used to create workers.

    Parameters:

    Name Type Description Default Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

    A function or coroutine.

    None str

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Cancel all workers in the same group.

    False str | None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    None bool

    Mark the method as a thread worker.

    False"},{"location":"api/logger/#textual.work(method)","title":"method","text":""},{"location":"api/logger/#textual.work(name)","title":"name","text":""},{"location":"api/logger/#textual.work(group)","title":"group","text":""},{"location":"api/logger/#textual.work(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/logger/#textual.work(exclusive)","title":"exclusive","text":""},{"location":"api/logger/#textual.work(description)","title":"description","text":""},{"location":"api/logger/#textual.work(thread)","title":"thread","text":""},{"location":"api/logging/","title":"textual.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","text":"
    TextualHandler(stderr=True, stdout=False)\n

    Bases: Handler

    A Logging handler for Textual apps.

    Parameters:

    Name Type Description Default bool

    Log to stderr when there is no active app.

    True bool

    Log to stdout when there is no active app.

    False"},{"location":"api/logging/#textual.logging.TextualHandler(stderr)","title":"stderr","text":""},{"location":"api/logging/#textual.logging.TextualHandler(stdout)","title":"stdout","text":""},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit","text":"
    emit(record)\n

    Invoked by logging.

    "},{"location":"api/map_geometry/","title":"textual.map_geometry","text":"

    A data structure returned by screen.find_widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry","title":"MapGeometry","text":"

    Bases: NamedTuple

    Defines the absolute location of a Widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.clip","title":"clip instance-attribute","text":"
    clip\n

    A region to clip the widget by (if a Widget is within a container).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.container_size","title":"container_size instance-attribute","text":"
    container_size\n

    The container size (area not occupied by scrollbars).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
    dock_gutter\n

    Space from the container reserved by docked widgets.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.order","title":"order instance-attribute","text":"
    order\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.map_geometry.MapGeometry.region","title":"region instance-attribute","text":"
    region\n

    The (screen) region occupied by the widget.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
    virtual_region\n

    The region relative to the container (but not necessarily visible).

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size\n

    The virtual size (scrollable area) of a widget if it is a container.

    "},{"location":"api/map_geometry/#textual.map_geometry.MapGeometry.visible_region","title":"visible_region property","text":"
    visible_region\n

    The Widget region after clipping.

    "},{"location":"api/message/","title":"textual.message","text":"

    The base class for all messages (including events).

    "},{"location":"api/message/#textual.message.Message","title":"Message","text":"
    Message()\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()\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\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\n

    Name of the default message handler.

    "},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
    is_forwarded\n

    Has the message been forwarded?

    "},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default","text":"
    prevent_default(prevent=True)\n

    Suppress the default action(s). This will prevent handlers in any base classes from being called.

    Parameters:

    Name Type Description Default bool

    True if the default action should be suppressed, or False if the default actions should be performed.

    True"},{"location":"api/message/#textual.message.Message.prevent_default(prevent)","title":"prevent","text":""},{"location":"api/message/#textual.message.Message.set_sender","title":"set_sender","text":"
    set_sender(sender)\n

    Set the sender of the message.

    Parameters:

    Name Type Description Default MessagePump

    The sender.

    required Note

    When creating a message the sender is automatically set. Normally there will be no need for this method to be called. This method will be used when strict control is required over the sender of a message.

    Returns:

    Type Description Self

    Self.

    "},{"location":"api/message/#textual.message.Message.set_sender(sender)","title":"sender","text":""},{"location":"api/message/#textual.message.Message.stop","title":"stop","text":"
    stop(stop=True)\n

    Stop propagation of the message to parent.

    Parameters:

    Name Type Description Default bool

    The stop flag.

    True"},{"location":"api/message/#textual.message.Message.stop(stop)","title":"stop","text":""},{"location":"api/message_pump/","title":"textual.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","text":"
    MessagePump(parent=None)\n

    Base class which supplies a message pump.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
    app\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\n

    Does this object have a parent?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
    is_attached\n

    Is this node linked to the app through the DOM?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_dom_root","title":"is_dom_root property","text":"
    is_dom_root\n

    Is this a root node (i.e. the App)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
    is_parent_active\n

    Is the parent active?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
    is_running\n

    Is the message pump running (potentially processing messages)?

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
    log\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\n

    The current size of the message queue.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.message_signal","title":"message_signal instance-attribute","text":"
    message_signal = Signal(self, 'messages')\n

    Subscribe to this signal to be notified of all messages sent to this widget.

    This is a fairly low-level mechanism, and shouldn't replace regular message handling.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh","text":"
    call_after_refresh(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:

    Name Type Description Default Callback

    A callable.

    required

    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_after_refresh(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later","text":"
    call_later(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:

    Name Type Description Default Callback

    Callable to call next.

    required Any

    Positional arguments to pass to the callable.

    () 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_later(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later(*args)","title":"*args","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later(**kwargs)","title":"**kwargs","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next","text":"
    call_next(callback, *args, **kwargs)\n

    Schedule a callback to run immediately after processing the current message.

    Parameters:

    Name Type Description Default Callback

    Callable to run after current event.

    required Any

    Positional arguments to pass to the callable.

    () Any

    Keyword arguments to pass to the callable.

    {}"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(*args)","title":"*args","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next(**kwargs)","title":"**kwargs","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle","text":"
    check_idle()\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","text":"
    check_message_enabled(message)\n

    Check if a given message is enabled (allowed to be sent).

    Parameters:

    Name Type Description Default Message

    A message object.

    required

    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.check_message_enabled(message)","title":"message","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages","text":"
    disable_messages(*messages)\n

    Disable message types from being processed.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages","text":"
    enable_messages(*messages)\n

    Enable processing of messages types.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
    on_event(event)\n

    Called to process an event.

    Parameters:

    Name Type Description Default Event

    An Event object.

    required"},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event(event)","title":"event","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message","text":"
    post_message(message)\n

    Posts a message on to this widget's queue.

    Parameters:

    Name Type Description Default Message

    A message (including Event).

    required

    Returns:

    Type Description bool

    True if the messages was processed, False if it wasn't.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message(message)","title":"message","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent","text":"
    prevent(*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","text":"
    set_interval(\n    interval,\n    callback=None,\n    *,\n    name=None,\n    repeat=0,\n    pause=False\n)\n

    Call a function at periodic intervals.

    Parameters:

    Name Type Description Default float

    Time (in seconds) between calls.

    required TimerCallback | None

    Function to call.

    None str | None

    Name of the timer object.

    None int

    Number of times to repeat the call or 0 for continuous.

    0 bool

    Start the timer paused.

    False

    Returns:

    Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(interval)","title":"interval","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(name)","title":"name","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(repeat)","title":"repeat","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval(pause)","title":"pause","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer","text":"
    set_timer(delay, callback=None, *, name=None, pause=False)\n

    Call a function after a delay.

    Example
    def ready():\n    self.notify(\"Your soft boiled egg is ready!\")\n# Call ready() after 3 minutes\nself.set_timer(3 * 60, ready)\n

    Parameters:

    Name Type Description Default float

    Time (in seconds) to wait before invoking callback.

    required TimerCallback | None

    Callback to call after time has expired.

    None str | None

    Name of the timer (for debug).

    None bool

    Start timer paused.

    False

    Returns:

    Type Description Timer

    A timer object.

    "},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(delay)","title":"delay","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(callback)","title":"callback","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(name)","title":"name","text":""},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer(pause)","title":"pause","text":""},{"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\", pane=\"#home\")\ndef switch_to_home(self) -> None:\n    self.log(\"Switching back to the home tab.\")\n    ...\n

    Parameters:

    Name Type Description Default type[Message]

    The message type (i.e. the class).

    required str | None

    An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

    None str

    Additional selectors for other attributes of the message.

    {}"},{"location":"api/on/#textual.on(message_type)","title":"message_type","text":""},{"location":"api/on/#textual.on(selector)","title":"selector","text":""},{"location":"api/on/#textual.on(**kwargs)","title":"**kwargs","text":""},{"location":"api/pilot/","title":"textual.pilot","text":"

    This module contains the Pilot class 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","text":"

    Bases: Exception

    Raised when the pilot mouse target is outside of the (visible) screen.

    "},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot","text":"
    Pilot(app)\n

    Bases: Generic[ReturnType]

    Pilot object to drive an app.

    "},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
    app\n
    "},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
    click(\n    widget=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:

    Name Type Description Default Widget | type[Widget] | str | None

    A widget or selector used as an origin 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.

    None tuple[int, int]

    The offset to click. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

    (0, 0) bool

    Click with the shift key held down.

    False bool

    Click with the meta key held down.

    False bool

    Click with the control key held down.

    False

    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.click(widget)","title":"widget","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.click(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
    exit(result)\n

    Exit the app with the given result.

    Parameters:

    Name Type Description Default ReturnType

    The app result returned by run or run_async.

    required"},{"location":"api/pilot/#textual.pilot.Pilot.exit(result)","title":"result","text":""},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
    hover(widget=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:

    Name Type Description Default Widget | type[Widget] | str | None | None

    A widget or selector used as an origin 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.

    None tuple[int, int]

    The offset to hover. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

    (0, 0)

    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.hover(widget)","title":"widget","text":""},{"location":"api/pilot/#textual.pilot.Pilot.hover(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down","title":"mouse_down async","text":"
    mouse_down(\n    widget=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:

    Name Type Description Default Widget | type[Widget] | str | None

    A widget or selector used as an origin 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.

    None tuple[int, int]

    The offset for the event. The offset is relative to the selector / widget provided or to the screen, if no selector is provided.

    (0, 0) bool

    Simulate the event with the shift key held down.

    False bool

    Simulate the event with the meta key held down.

    False bool

    Simulate the event with the control key held down.

    False

    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_down(widget)","title":"widget","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_down(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up","title":"mouse_up async","text":"
    mouse_up(\n    widget=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:

    Name Type Description Default Widget | type[Widget] | str | None

    A widget or selector used as an origin 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.

    None tuple[int, int]

    The offset for the event. The offset is relative to the widget / selector provided or to the screen, if no selector is provided.

    (0, 0) bool

    Simulate the event with the shift key held down.

    False bool

    Simulate the event with the meta key held down.

    False bool

    Simulate the event with the control key held down.

    False

    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(widget)","title":"widget","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(offset)","title":"offset","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(shift)","title":"shift","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(meta)","title":"meta","text":""},{"location":"api/pilot/#textual.pilot.Pilot.mouse_up(control)","title":"control","text":""},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
    pause(delay=None)\n

    Insert a pause.

    Parameters:

    Name Type Description Default float | None

    Seconds to pause, or None to wait for cpu idle.

    None"},{"location":"api/pilot/#textual.pilot.Pilot.pause(delay)","title":"delay","text":""},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
    press(*keys)\n

    Simulate key-presses.

    Parameters:

    Name Type Description Default str

    Keys to press.

    ()"},{"location":"api/pilot/#textual.pilot.Pilot.press(*keys)","title":"*keys","text":""},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal","title":"resize_terminal async","text":"
    resize_terminal(width, height)\n

    Resize the terminal to the given dimensions.

    Parameters:

    Name Type Description Default int

    The new width of the terminal.

    required int

    The new height of the terminal.

    required"},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal(width)","title":"width","text":""},{"location":"api/pilot/#textual.pilot.Pilot.resize_terminal(height)","title":"height","text":""},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
    wait_for_animation()\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":"
    wait_for_scheduled_animations()\n

    Wait for any current and scheduled animations to complete.

    "},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout","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":"textual.css.query","text":"

    This module contains the DOMQuery class and related objects.

    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","text":"
    DOMQuery(\n    node,\n    *,\n    filter=None,\n    exclude=None,\n    deep=True,\n    parent=None\n)\n

    Bases: Generic[QueryType]

    Warning

    You won't need to construct this manually, as DOMQuery objects are returned by query.

    Parameters:

    Name Type Description Default DOMNode

    A DOM node.

    required str | None

    Query to filter children in the node.

    None str | None

    Query to exclude children in the node.

    None bool

    Query should be deep, i.e. recursive.

    True DOMQuery | None

    The parent query, if this is the result of filtering another query.

    None

    Raises:

    Type Description InvalidQueryFormat

    If the format of the query is invalid.

    "},{"location":"api/query/#textual.css.query.DOMQuery(node)","title":"node","text":""},{"location":"api/query/#textual.css.query.DOMQuery(filter)","title":"filter","text":""},{"location":"api/query/#textual.css.query.DOMQuery(exclude)","title":"exclude","text":""},{"location":"api/query/#textual.css.query.DOMQuery(deep)","title":"deep","text":""},{"location":"api/query/#textual.css.query.DOMQuery(parent)","title":"parent","text":""},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
    node\n

    The node being queried.

    "},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
    nodes\n

    Lazily evaluate nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class","text":"
    add_class(*class_names)\n

    Add the given class name(s) to nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.blur","title":"blur","text":"
    blur()\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","text":"
    exclude(selector)\n

    Exclude nodes that match a given selector.

    Parameters:

    Name Type Description Default str

    A CSS selector.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    New DOM query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.exclude(selector)","title":"selector","text":""},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter","text":"
    filter(selector)\n

    Filter this set by the given CSS selector.

    Parameters:

    Name Type Description Default str

    A CSS selector.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    New DOM Query.

    "},{"location":"api/query/#textual.css.query.DOMQuery.filter(selector)","title":"selector","text":""},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first","text":"
    first() -> QueryType\n
    first(expect_type: type[ExpectType]) -> ExpectType\n
    first(expect_type=None)\n

    Get the first matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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.first(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.focus","title":"focus","text":"
    focus()\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","text":"
    last() -> QueryType\n
    last(expect_type: type[ExpectType]) -> ExpectType\n
    last(expect_type=None)\n

    Get the last matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one","text":"
    only_one() -> QueryType\n
    only_one(expect_type: type[ExpectType]) -> ExpectType\n
    only_one(expect_type=None)\n

    Get the only matching node.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    Require matched node is of this type, or None for any type.

    None

    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.only_one(expect_type)","title":"expect_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh","text":"
    refresh(*, repaint=True, layout=False, recompose=False)\n

    Refresh matched nodes.

    Parameters:

    Name Type Description Default bool

    Repaint node(s).

    True bool

    Layout node(s).

    False bool

    Recompose node(s).

    False

    Returns:

    Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.refresh(repaint)","title":"repaint","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh(layout)","title":"layout","text":""},{"location":"api/query/#textual.css.query.DOMQuery.refresh(recompose)","title":"recompose","text":""},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove","text":"
    remove()\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","text":"
    remove_class(*class_names)\n

    Remove the given class names from the nodes.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results","text":"
    results() -> Iterator[QueryType]\n
    results(\n    filter_type: type[ExpectType],\n) -> Iterator[ExpectType]\n
    results(filter_type=None)\n

    Get query results, optionally filtered by a given type.

    Parameters:

    Name Type Description Default type[ExpectType] | None

    A Widget class to filter results, or None for no filter.

    None

    Yields:

    Type Description QueryType | ExpectType

    Iterator[Widget | ExpectType]: An iterator of Widget instances.

    "},{"location":"api/query/#textual.css.query.DOMQuery.results(filter_type)","title":"filter_type","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set","title":"set","text":"
    set(\n    display=None, visible=None, disabled=None, loading=None\n)\n

    Sets common attributes on matched nodes.

    Parameters:

    Name Type Description Default bool | None

    Set display attribute on nodes, or None for no change.

    None bool | None

    Set visible attribute on nodes, or None for no change.

    None bool | None

    Set disabled attribute on nodes, or None for no change.

    None bool | None

    Set loading attribute on nodes, or None for no change.

    None

    Returns:

    Type Description DOMQuery[QueryType]

    Query for chaining.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set(display)","title":"display","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(visible)","title":"visible","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(disabled)","title":"disabled","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set(loading)","title":"loading","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class","text":"
    set_class(add, *class_names)\n

    Set the given class name(s) according to a condition.

    Parameters:

    Name Type Description Default bool

    Add the classes if True, otherwise remove them.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_class(add)","title":"add","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes","text":"
    set_classes(classes)\n

    Set the classes on nodes to exactly the given set.

    Parameters:

    Name Type Description Default str | Iterable[str]

    A string of space separated classes, or an iterable of class names.

    required

    Returns:

    Type Description DOMQuery[QueryType]

    Self.

    "},{"location":"api/query/#textual.css.query.DOMQuery.set_classes(classes)","title":"classes","text":""},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles","text":"
    set_styles(css=None, **update_styles)\n

    Set styles on matched nodes.

    Parameters:

    Name Type Description Default str | None

    CSS declarations to parser, or None.

    None"},{"location":"api/query/#textual.css.query.DOMQuery.set_styles(css)","title":"css","text":""},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class","text":"
    toggle_class(*class_names)\n

    Toggle the given class names from matched nodes.

    "},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat","text":"

    Bases: QueryError

    Query did not parse correctly.

    "},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches","text":"

    Bases: QueryError

    No nodes matched the query.

    "},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError","text":"

    Bases: Exception

    Base class for a query related error.

    "},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches","text":"

    Bases: QueryError

    Too many nodes matched the query.

    "},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType","text":"

    Bases: QueryError

    Query result was not of the correct type.

    "},{"location":"api/reactive/","title":"textual.reactive","text":"

    This module contains the Reactive class which implements reactivity.

    "},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive","text":"
    Reactive(\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=False,\n    always_update=False,\n    compute=True,\n    recompose=False,\n    bindings=False\n)\n

    Bases: Generic[ReactiveType]

    Reactive descriptor.

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Perform a layout on change.

    False bool

    Perform a repaint on change.

    True bool

    Call watchers on initialize (post mount).

    False bool

    Call watchers even when the new value equals the old value.

    False bool

    Run compute methods when attribute is changed.

    True bool

    Compose the widget again when the attribute changes.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.Reactive(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.Reactive(layout)","title":"layout","text":""},{"location":"api/reactive/#textual.reactive.Reactive(repaint)","title":"repaint","text":""},{"location":"api/reactive/#textual.reactive.Reactive(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.Reactive(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.Reactive(compute)","title":"compute","text":""},{"location":"api/reactive/#textual.reactive.Reactive(recompose)","title":"recompose","text":""},{"location":"api/reactive/#textual.reactive.Reactive(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.Reactive.owner","title":"owner property","text":"
    owner\n

    The owner (class) where the reactive was declared.

    "},{"location":"api/reactive/#textual.reactive.ReactiveError","title":"ReactiveError","text":"

    Bases: Exception

    Base class for reactive errors.

    "},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError","text":"

    Bases: ReactiveError

    Raised when an attribute has public and private compute methods.

    "},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive","text":"
    reactive(\n    default,\n    *,\n    layout=False,\n    repaint=True,\n    init=True,\n    always_update=False,\n    recompose=False,\n    bindings=False\n)\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute.

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Perform a layout on change.

    False bool

    Perform a repaint on change.

    True bool

    Call watchers on initialize (post mount).

    True bool

    Call watchers even when the new value equals the old value.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.reactive(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.reactive(layout)","title":"layout","text":""},{"location":"api/reactive/#textual.reactive.reactive(repaint)","title":"repaint","text":""},{"location":"api/reactive/#textual.reactive.reactive(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.reactive(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.reactive(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.var","title":"var","text":"
    var(\n    default, init=True, always_update=False, bindings=False\n)\n

    Bases: Reactive[ReactiveType]

    Create a reactive attribute (with no auto-refresh).

    Parameters:

    Name Type Description Default ReactiveType | Callable[[], ReactiveType]

    A default value or callable that returns a default.

    required bool

    Call watchers on initialize (post mount).

    True bool

    Call watchers even when the new value equals the old value.

    False bool

    Refresh bindings when the reactive changes.

    False"},{"location":"api/reactive/#textual.reactive.var(default)","title":"default","text":""},{"location":"api/reactive/#textual.reactive.var(init)","title":"init","text":""},{"location":"api/reactive/#textual.reactive.var(always_update)","title":"always_update","text":""},{"location":"api/reactive/#textual.reactive.var(bindings)","title":"bindings","text":""},{"location":"api/reactive/#textual.reactive.await_watcher","title":"await_watcher async","text":"
    await_watcher(obj, awaitable)\n

    Coroutine to await an awaitable returned from a watcher

    "},{"location":"api/reactive/#textual.reactive.invoke_watcher","title":"invoke_watcher","text":"
    invoke_watcher(\n    watcher_object, watch_function, old_value, value\n)\n

    Invoke a watch function.

    Parameters:

    Name Type Description Default Reactable

    The object watching for the changes.

    required WatchCallbackType

    A watch function, which may be sync or async.

    required object

    The old value of the attribute.

    required object

    The new value of the attribute.

    required"},{"location":"api/reactive/#textual.reactive.invoke_watcher(watcher_object)","title":"watcher_object","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(watch_function)","title":"watch_function","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(old_value)","title":"old_value","text":""},{"location":"api/reactive/#textual.reactive.invoke_watcher(value)","title":"value","text":""},{"location":"api/renderables/","title":"textual.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","text":"
    Bar(\n    highlight_range=(0, 0),\n    highlight_style=\"magenta\",\n    background_style=\"grey37\",\n    clickable_ranges=None,\n    width=None,\n    gradient=None,\n)\n

    Thin horizontal bar with a portion highlighted.

    Parameters:

    Name Type Description Default tuple[float, float]

    The range to highlight.

    (0, 0) StyleType

    The style of the highlighted range of the bar.

    'magenta' StyleType

    The style of the non-highlighted range(s) of the bar.

    'grey37' int | None

    The width of the bar, or None to fill available width.

    None Gradient | None

    Optional gradient object.

    None"},{"location":"api/renderables/#textual.renderables.bar.Bar(highlight_range)","title":"highlight_range","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(highlight_style)","title":"highlight_style","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(background_style)","title":"background_style","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(width)","title":"width","text":""},{"location":"api/renderables/#textual.renderables.bar.Bar(gradient)","title":"gradient","text":""},{"location":"api/renderables/#textual.renderables.blank.Blank","title":"Blank","text":"
    Blank(color='transparent')\n

    Draw solid background color.

    "},{"location":"api/renderables/#textual.renderables.digits.Digits","title":"Digits","text":"
    Digits(text, style='')\n

    Renders a 3X3 unicode 'font' for numerical values.

    Parameters:

    Name Type Description Default str

    Text to display.

    required StyleType

    Style to apply to the digits.

    ''"},{"location":"api/renderables/#textual.renderables.digits.Digits(text)","title":"text","text":""},{"location":"api/renderables/#textual.renderables.digits.Digits(style)","title":"style","text":""},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width","title":"get_width classmethod","text":"
    get_width(text)\n

    Calculate the width without rendering.

    Parameters:

    Name Type Description Default str

    Text which may be displayed in the Digits widget.

    required

    Returns:

    Type Description int

    width of the text (in cells).

    "},{"location":"api/renderables/#textual.renderables.digits.Digits.get_width(text)","title":"text","text":""},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient","title":"LinearGradient","text":"
    LinearGradient(angle, stops)\n

    Render a linear gradient with a rotation.

    Parameters:

    Name Type Description Default float

    Angle of rotation in degrees.

    required Sequence[tuple[float, Color | str]]

    List of stop consisting of pairs of offset (between 0 and 1) and color.

    required"},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient(angle)","title":"angle","text":""},{"location":"api/renderables/#textual.renderables.gradient.LinearGradient(stops)","title":"stops","text":""},{"location":"api/renderables/#textual.renderables.gradient.VerticalGradient","title":"VerticalGradient","text":"
    VerticalGradient(color1, color2)\n

    Draw a vertical gradient.

    "},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline","title":"Sparkline","text":"
    Sparkline(\n    data,\n    *,\n    width,\n    min_color=from_rgb(0, 255, 0),\n    max_color=from_rgb(255, 0, 0),\n    summary_function=max\n)\n

    Bases: Generic[T]

    A sparkline representing a series of data.

    Parameters:

    Name Type Description Default Sequence[T]

    The sequence of data to render.

    required int | None

    The width of the sparkline/the number of buckets to partition the data into.

    required Color

    The color of values equal to the min value in data.

    from_rgb(0, 255, 0) Color

    The color of values equal to the max value in data.

    from_rgb(255, 0, 0) SummaryFunction[T]

    Function that will be applied to each bucket.

    max"},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(data)","title":"data","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(width)","title":"width","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(min_color)","title":"min_color","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(max_color)","title":"max_color","text":""},{"location":"api/renderables/#textual.renderables.sparkline.Sparkline(summary_function)","title":"summary_function","text":""},{"location":"api/screen/","title":"textual.screen","text":"

    This module contains the Screen class and related objects.

    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[[Optional[ScreenResultType]], None],\n    Callable[[Optional[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","text":"
    ModalScreen(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","text":"
    ResultCallback(requester, callback, future=None)\n

    Bases: Generic[ScreenResultType]

    Holds the details of a callback.

    Parameters:

    Name Type Description Default MessagePump

    The object making a request for the callback.

    required ScreenResultCallbackType[ScreenResultType] | None

    The callback function.

    required Future[ScreenResultType] | None

    A Future to hold the result.

    None"},{"location":"api/screen/#textual.screen.ResultCallback(requester)","title":"requester","text":""},{"location":"api/screen/#textual.screen.ResultCallback(callback)","title":"callback","text":""},{"location":"api/screen/#textual.screen.ResultCallback(future)","title":"future","text":""},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
    callback = 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","text":"
    Screen(name=None, id=None, classes=None)\n

    Bases: Generic[ScreenResultType], Widget

    The base class for screens.

    Parameters:

    Name Type Description Default str | None

    The name of the screen.

    None str | None

    The ID of the screen in the DOM.

    None str | None

    The CSS classes for the screen.

    None"},{"location":"api/screen/#textual.screen.Screen(name)","title":"name","text":""},{"location":"api/screen/#textual.screen.Screen(id)","title":"id","text":""},{"location":"api/screen/#textual.screen.Screen(classes)","title":"classes","text":""},{"location":"api/screen/#textual.screen.Screen.ALLOW_IN_MAXIMIZED_VIEW","title":"ALLOW_IN_MAXIMIZED_VIEW class-attribute","text":"
    ALLOW_IN_MAXIMIZED_VIEW = None\n

    A selector for the widgets (direct children of Screen) that are allowed in the maximized view (in addition to maximized widget). Or None to default to App.ALLOW_IN_MAXIMIZED_VIEW

    "},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
    AUTO_FOCUS = 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

    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 = ''\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 = None\n

    File paths to load CSS from.

    Note

    This CSS applies to the whole app.

    "},{"location":"api/screen/#textual.screen.Screen.ESCAPE_TO_MINIMIZE","title":"ESCAPE_TO_MINIMIZE class-attribute","text":"
    ESCAPE_TO_MINIMIZE = None\n

    Use escape key to minimize (potentially overriding bindings) or None to defer to App.ESCAPE_TO_MINIMIZE.

    "},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
    SUB_TITLE = 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 = 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.active_bindings","title":"active_bindings property","text":"
    active_bindings\n

    Get currently active bindings for this screen.

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

    This property may be used to inspect current bindings.

    Returns:

    Type Description dict[str, ActiveBinding]

    A map of keys to a tuple containing (NAMESPACE, BINDING, ENABLED).

    "},{"location":"api/screen/#textual.screen.Screen.bindings_updated_signal","title":"bindings_updated_signal instance-attribute","text":"
    bindings_updated_signal = Signal(self, 'bindings_updated')\n

    A signal published when the bindings have been updated

    "},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
    focus_chain\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(None)\n

    The focused widget or None for no focus. To set focus, do not update this value directly. Use set_focus instead.

    "},{"location":"api/screen/#textual.screen.Screen.is_active","title":"is_active property","text":"
    is_active\n

    Is the screen active (i.e. visible and top of the stack)?

    "},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
    is_current\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\n

    Is the screen modal?

    "},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
    layers\n

    Layers from parent.

    Returns:

    Type Description tuple[str, ...]

    Tuple of layer names.

    "},{"location":"api/screen/#textual.screen.Screen.maximized","title":"maximized class-attribute instance-attribute","text":"
    maximized = Reactive(None, layout=True)\n

    The currently maximized widget, or None for no maximized widget.

    "},{"location":"api/screen/#textual.screen.Screen.screen_layout_refresh_signal","title":"screen_layout_refresh_signal instance-attribute","text":"
    screen_layout_refresh_signal = Signal(\n    self, \"layout-refresh\"\n)\n

    The signal that is published when the screen's layout is refreshed.

    "},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates class-attribute instance-attribute","text":"
    stack_updates = 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 = 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 = TITLE\n

    Screen title to override the app title.

    "},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss async","text":"
    action_dismiss(result=None)\n

    A wrapper around dismiss that can be called as an action.

    Parameters:

    Name Type Description Default ScreenResultType | None

    The optional result to be passed to the result callback.

    None"},{"location":"api/screen/#textual.screen.Screen.action_dismiss(result)","title":"result","text":""},{"location":"api/screen/#textual.screen.Screen.action_maximize","title":"action_maximize","text":"
    action_maximize()\n

    Action to maximize the currently focused widget.

    "},{"location":"api/screen/#textual.screen.Screen.action_minimize","title":"action_minimize","text":"
    action_minimize()\n

    Action to minimize the currently maximized widget.

    "},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view","text":"
    can_view(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:

    Name Type Description Default Widget

    A widget that is a descendant of self.

    required

    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.can_view(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss","text":"
    dismiss(result=None)\n

    Dismiss the screen, optionally with a result.

    Any callback provided in push_screen will be invoked with the supplied result.

    Only the active screen may be dismissed. This method will produce a warning in the logs if called on an inactive screen (but otherwise have no effect).

    Warning

    Textual will raise a ScreenError if you await the return value from a message handler on the Screen being dismissed. If you want to dismiss the current screen, you can call self.dismiss() without awaiting.

    Parameters:

    Name Type Description Default ScreenResultType | None

    The optional result to be passed to the result callback.

    None"},{"location":"api/screen/#textual.screen.Screen.dismiss(result)","title":"result","text":""},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget","text":"
    find_widget(widget)\n

    Get the screen region of a Widget.

    Parameters:

    Name Type Description Default Widget

    A Widget within the composition.

    required

    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.find_widget(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next","text":"
    focus_next(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:

    Name Type Description Default 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_next(selector)","title":"selector","text":""},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous","text":"
    focus_previous(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:

    Name Type Description Default 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(selector)","title":"selector","text":""},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at","title":"get_focusable_widget_at","text":"
    get_focusable_widget_at(x, y)\n

    Get the focusable widget under a given coordinate.

    If the widget directly under the given coordinate is not focusable, then this method will check if any of the ancestors are focusable. If no ancestors are focusable, then None will be returned.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description Widget | None

    A Widget, or None if there is no focusable widget underneath the coordinate.

    "},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_focusable_widget_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset","text":"
    get_offset(widget)\n

    Get the absolute offset of a given Widget.

    Parameters:

    Name Type Description Default Widget

    A widget

    required

    Returns:

    Type Description Offset

    The widget's offset relative to the top left of the terminal.

    "},{"location":"api/screen/#textual.screen.Screen.get_offset(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at","text":"
    get_style_at(x, y)\n

    Get the style under a given coordinate.

    Parameters:

    Name Type Description Default int

    X Coordinate.

    required int

    Y Coordinate.

    required

    Returns:

    Type Description Style

    Rich Style object.

    "},{"location":"api/screen/#textual.screen.Screen.get_style_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_style_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at","text":"
    get_widget_at(x, y)\n

    Get the widget at a given coordinate.

    Parameters:

    Name Type Description Default int

    X Coordinate.

    required int

    Y Coordinate.

    required

    Returns:

    Type Description tuple[Widget, Region]

    Widget and screen region.

    Raises:

    Type Description NoWidget

    If there is no widget under the screen coordinate.

    "},{"location":"api/screen/#textual.screen.Screen.get_widget_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_widget_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at","text":"
    get_widgets_at(x, y)\n

    Get all widgets under a given coordinate.

    Parameters:

    Name Type Description Default int

    X coordinate.

    required int

    Y coordinate.

    required

    Returns:

    Type Description Iterable[tuple[Widget, Region]]

    Sequence of (WIDGET, REGION) tuples.

    "},{"location":"api/screen/#textual.screen.Screen.get_widgets_at(x)","title":"x","text":""},{"location":"api/screen/#textual.screen.Screen.get_widgets_at(y)","title":"y","text":""},{"location":"api/screen/#textual.screen.Screen.maximize","title":"maximize","text":"
    maximize(widget, container=True)\n

    Maximize a widget, so it fills the screen.

    Parameters:

    Name Type Description Default Widget

    Widget to maximize.

    required bool

    If one of the widgets ancestors is a maximizeable widget, maximize that instead.

    True"},{"location":"api/screen/#textual.screen.Screen.maximize(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.maximize(container)","title":"container","text":""},{"location":"api/screen/#textual.screen.Screen.minimize","title":"minimize","text":"
    minimize()\n

    Restore any maximized widget to normal state.

    "},{"location":"api/screen/#textual.screen.Screen.pop_until_active","title":"pop_until_active","text":"
    pop_until_active()\n

    Pop any screens on top of this one, until this screen is active.

    Raises:

    Type Description ScreenError

    If this screen is not in the current mode.

    "},{"location":"api/screen/#textual.screen.Screen.refresh_bindings","title":"refresh_bindings","text":"
    refresh_bindings()\n

    Call to request a refresh of bindings.

    "},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus","text":"
    set_focus(widget, scroll_visible=True)\n

    Focus (or un-focus) a widget. A focused widget will receive key events first.

    Parameters:

    Name Type Description Default Widget | None

    Widget to focus, or None to un-focus.

    required bool

    Scroll widget in to view.

    True"},{"location":"api/screen/#textual.screen.Screen.set_focus(widget)","title":"widget","text":""},{"location":"api/screen/#textual.screen.Screen.set_focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title","text":"
    validate_sub_title(sub_title)\n

    Ensure the sub-title is a string or None.

    "},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title","text":"
    validate_title(title)\n

    Ensure the title is a string or None.

    "},{"location":"api/screen/#textual.screen.SystemModalScreen","title":"SystemModalScreen","text":"
    SystemModalScreen(name=None, id=None, classes=None)\n

    Bases: ModalScreen[ScreenResultType]

    A variant of ModalScreen for internal use.

    This version of ModalScreen allows us to build system-level screens; the type being used to indicate that the screen should be isolated from the main application.

    Note

    This screen is set to not inherit CSS.

    "},{"location":"api/scroll_view/","title":"textual.scroll_view","text":"

    ScrollView is a base class for Line API widgets.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView","text":"
    ScrollView(\n    *children,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n)\n

    Bases: ScrollableContainer

    A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(*children)","title":"*children","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(name)","title":"name","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(id)","title":"id","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(classes)","title":"classes","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView(disabled)","title":"disabled","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable\n

    Always scrollable.

    "},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_line","title":"refresh_line","text":"
    refresh_line(y)\n

    Refresh a single line.

    Parameters:

    Name Type Description Default int

    Coordinate of line.

    required"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_line(y)","title":"y","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines","text":"
    refresh_lines(y_start, line_count=1)\n

    Refresh one or more lines.

    Parameters:

    Name Type Description Default int

    First line to refresh.

    required int

    Total number of lines to refresh.

    1"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines(y_start)","title":"y_start","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines(line_count)","title":"line_count","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to","text":"
    scroll_to(\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    level=\"basic\"\n)\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters:

    Name Type Description Default float | None

    X coordinate (column) to scroll to, or None for no change.

    None float | None

    Y coordinate (row) to scroll to, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(x)","title":"x","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(y)","title":"y","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(animate)","title":"animate","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(speed)","title":"speed","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(duration)","title":"duration","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(easing)","title":"easing","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(force)","title":"force","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(on_complete)","title":"on_complete","text":""},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to(level)","title":"level","text":""},{"location":"api/scrollbar/","title":"textual.scrollbar","text":"

    Contains the widgets that manage Textual scrollbars.

    Note

    You will not typically need this for most apps.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar","text":"
    ScrollBar(vertical=True, name=None, *, thickness=1)\n

    Bases: Widget

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
    renderer = 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","text":"
    action_grab()\n

    Begin capturing the mouse cursor.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down","text":"
    action_scroll_down()\n

    Scroll vertical scrollbars down, horizontal scrollbars right.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up","text":"
    action_scroll_up()\n

    Scroll vertical scrollbars up, horizontal scrollbars left.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner","text":"
    ScrollBarCorner(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.ScrollBarRender","title":"ScrollBarRender","text":"
    ScrollBarRender(\n    virtual_size=100,\n    window_size=0,\n    position=0,\n    thickness=1,\n    vertical=True,\n    style=\"bright_magenta on #555555\",\n)\n
    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.BLANK_GLYPH","title":"BLANK_GLYPH class-attribute","text":"
    BLANK_GLYPH = ' '\n

    Glyph used for the main body of the scrollbar

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.HORIZONTAL_BARS","title":"HORIZONTAL_BARS class-attribute","text":"
    HORIZONTAL_BARS = ['\u2589', '\u258a', '\u258b', '\u258c', '\u258d', '\u258e', '\u258f', ' ']\n

    Glyphs used for horizontal scrollbar ends, for smoother display.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarRender.VERTICAL_BARS","title":"VERTICAL_BARS class-attribute","text":"
    VERTICAL_BARS = ['\u2581', '\u2582', '\u2583', '\u2584', '\u2585', '\u2586', '\u2587', ' ']\n

    Glyphs used for vertical scrollbar ends, for smoother display.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown","text":"
    ScrollDown()\n

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft","text":"
    ScrollLeft()\n

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage","text":"
    ScrollMessage()\n

    Bases: Message

    Base class for all scrollbar messages.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight","text":"
    ScrollRight()\n

    Bases: ScrollMessage

    Message sent when clicking below handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo","text":"
    ScrollTo(x=None, y=None, animate=True)\n

    Bases: ScrollMessage

    Message sent when click and dragging handle.

    "},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp","text":"
    ScrollUp()\n

    Bases: ScrollMessage

    Message sent when clicking above handle.

    "},{"location":"api/signal/","title":"textual.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","text":"
    Signal(owner, name)\n

    Bases: Generic[SignalT]

    A signal that a widget may subscribe to, in order to invoke callbacks when an associated event occurs.

    Parameters:

    Name Type Description Default DOMNode

    The owner of this signal.

    required str

    An identifier for debugging purposes.

    required"},{"location":"api/signal/#textual.signal.Signal(owner)","title":"owner","text":""},{"location":"api/signal/#textual.signal.Signal(name)","title":"name","text":""},{"location":"api/signal/#textual.signal.Signal.owner","title":"owner property","text":"
    owner\n

    The owner of this Signal, or None if there is no owner.

    "},{"location":"api/signal/#textual.signal.Signal.publish","title":"publish","text":"
    publish(data)\n

    Publish the signal (invoke subscribed callbacks).

    Parameters:

    Name Type Description Default SignalT

    An argument to pass to the callbacks.

    required"},{"location":"api/signal/#textual.signal.Signal.publish(data)","title":"data","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe","title":"subscribe","text":"
    subscribe(node, callback, immediate=False)\n

    Subscribe a node to this signal.

    When the signal is published, the callback will be invoked.

    Parameters:

    Name Type Description Default MessagePump

    Node to subscribe.

    required SignalCallbackType

    A callback function which takes a single argument and returns anything (return type ignored).

    required bool

    Invoke the callback immediately on publish if True, otherwise post it to the DOM node to be called once existing messages have been processed.

    False

    Raises:

    Type Description SignalError

    Raised when subscribing a non-mounted widget.

    "},{"location":"api/signal/#textual.signal.Signal.subscribe(node)","title":"node","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe(callback)","title":"callback","text":""},{"location":"api/signal/#textual.signal.Signal.subscribe(immediate)","title":"immediate","text":""},{"location":"api/signal/#textual.signal.Signal.unsubscribe","title":"unsubscribe","text":"
    unsubscribe(node)\n

    Unsubscribe a node from this signal.

    Parameters:

    Name Type Description Default MessagePump

    Node to unsubscribe,

    required"},{"location":"api/signal/#textual.signal.Signal.unsubscribe(node)","title":"node","text":""},{"location":"api/signal/#textual.signal.SignalError","title":"SignalError","text":"

    Bases: Exception

    Raised for Signal errors.

    "},{"location":"api/strip/","title":"textual.strip","text":"

    This module contains the Strip class and related objects.

    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","text":"
    Strip(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:

    Name Type Description Default Iterable[Segment]

    An iterable of segments.

    required int | None

    The cell length if known, or None to calculate on demand.

    None"},{"location":"api/strip/#textual.strip.Strip(segments)","title":"segments","text":""},{"location":"api/strip/#textual.strip.Strip(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
    cell_length\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\n

    A set of the link ids in this Strip.

    "},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
    text\n

    Segment text.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length","text":"
    adjust_cell_length(cell_length, style=None)\n

    Adjust the cell length, possibly truncating or extending.

    Parameters:

    Name Type Description Default int

    New desired cell length.

    required Style | None

    Style when extending, or None.

    None

    Returns:

    Type Description Strip

    A new strip with the supplied cell length.

    "},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter","text":"
    apply_filter(filter, background)\n

    Apply a filter to all segments in the strip.

    Parameters:

    Name Type Description Default LineFilter

    A line filter object.

    required

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_filter(filter)","title":"filter","text":""},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style","text":"
    apply_style(style)\n

    Apply a style to the Strip.

    Parameters:

    Name Type Description Default Style

    A Rich style.

    required

    Returns:

    Type Description Strip

    A new strip.

    "},{"location":"api/strip/#textual.strip.Strip.apply_style(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
    blank(cell_length, style=None)\n

    Create a blank strip.

    Parameters:

    Name Type Description Default int

    Desired cell length.

    required StyleType | None

    Style of blank.

    None

    Returns:

    Type Description Strip

    New strip.

    "},{"location":"api/strip/#textual.strip.Strip.blank(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.blank(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop","text":"
    crop(start, end=None)\n

    Crop a strip between two cell positions.

    Parameters:

    Name Type Description Default int

    The start cell position (inclusive).

    required int | None

    The end cell position (exclusive).

    None

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop(start)","title":"start","text":""},{"location":"api/strip/#textual.strip.Strip.crop(end)","title":"end","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend","text":"
    crop_extend(start, end, style)\n

    Crop between two points, extending the length if required.

    Parameters:

    Name Type Description Default int

    Start offset of crop.

    required int

    End offset of crop.

    required Style | None

    Style of additional padding.

    required

    Returns:

    Type Description Strip

    New cropped Strip.

    "},{"location":"api/strip/#textual.strip.Strip.crop_extend(start)","title":"start","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend(end)","title":"end","text":""},{"location":"api/strip/#textual.strip.Strip.crop_extend(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide","text":"
    divide(cuts)\n

    Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

    Parameters:

    Name Type Description Default Iterable[int]

    An iterable of cell positions as ints.

    required

    Returns:

    Type Description Sequence[Strip]

    A new list of strips.

    "},{"location":"api/strip/#textual.strip.Strip.divide(cuts)","title":"cuts","text":""},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length","text":"
    extend_cell_length(cell_length, style=None)\n

    Extend the cell length if it is less than the given value.

    Parameters:

    Name Type Description Default int

    Required minimum cell length.

    required Style | None

    Style for padding if the cell length is extended.

    None

    Returns:

    Type Description Strip

    A new Strip.

    "},{"location":"api/strip/#textual.strip.Strip.extend_cell_length(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.extend_cell_length(style)","title":"style","text":""},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
    from_lines(lines, cell_length=None)\n

    Convert lines (lists of segments) to a list of Strips.

    Parameters:

    Name Type Description Default list[list[Segment]]

    List of lines, where a line is a list of segments.

    required int | None

    Cell length of lines (must be same) or None if not known.

    None

    Returns:

    Type Description list[Strip]

    List of strips.

    "},{"location":"api/strip/#textual.strip.Strip.from_lines(lines)","title":"lines","text":""},{"location":"api/strip/#textual.strip.Strip.from_lines(cell_length)","title":"cell_length","text":""},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position","text":"
    index_to_cell_position(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:

    Name Type Description Default int

    The index to convert.

    required

    Returns:

    Type Description int

    The cell position of the character at index.

    "},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position(index)","title":"index","text":""},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
    join(strips)\n

    Join a number of strips in to one.

    Parameters:

    Name Type Description Default Iterable[Strip | None]

    An iterable of Strips.

    required

    Returns:

    Type Description Strip

    A new combined strip.

    "},{"location":"api/strip/#textual.strip.Strip.join(strips)","title":"strips","text":""},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify","text":"
    simplify()\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","text":"
    style_links(link_id, link_style)\n

    Apply a style to Segments with the given link_id.

    Parameters:

    Name Type Description Default str

    A link id.

    required Style

    Style to apply.

    required

    Returns:

    Type Description Strip

    New strip (or same Strip if no changes).

    "},{"location":"api/strip/#textual.strip.Strip.style_links(link_id)","title":"link_id","text":""},{"location":"api/strip/#textual.strip.Strip.style_links(link_style)","title":"link_style","text":""},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable","text":"
    StripRenderable(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","text":"
    get_line_length(segments)\n

    Get the line length (total length of all segments).

    Parameters:

    Name Type Description Default Iterable[Segment]

    Iterable of segments.

    required

    Returns:

    Type Description int

    Length of line in cells.

    "},{"location":"api/strip/#textual.strip.get_line_length(segments)","title":"segments","text":""},{"location":"api/suggester/","title":"textual.suggester","text":"

    Contains the Suggester class, used by the Input widget.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList","text":"
    SuggestFromList(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:

    Name Type Description Default Iterable[str]

    Valid suggestions sorted by decreasing priority.

    required bool

    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.

    True"},{"location":"api/suggester/#textual.suggester.SuggestFromList(suggestions)","title":"suggestions","text":""},{"location":"api/suggester/#textual.suggester.SuggestFromList(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
    get_suggestion(value)\n

    Gets a completion from the given possibilities.

    Parameters:

    Name Type Description Default str

    The current value.

    required

    Returns:

    Type Description str | None

    A valid completion suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion(value)","title":"value","text":""},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester","text":"
    Suggester(*, 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:

    Name Type Description Default bool

    Whether to cache suggestion results.

    True bool

    Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

    False"},{"location":"api/suggester/#textual.suggester.Suggester(use_cache)","title":"use_cache","text":""},{"location":"api/suggester/#textual.suggester.Suggester(case_sensitive)","title":"case_sensitive","text":""},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
    cache = LRUCache(1024) if use_cache else None\n

    Suggestion cache, if used.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion abstractmethod async","text":"
    get_suggestion(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:

    Name Type Description Default str

    The current value of the requester widget.

    required

    Returns:

    Type Description str | None

    A valid suggestion or None.

    "},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion(value)","title":"value","text":""},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady dataclass","text":"
    SuggestionReady(value, suggestion)\n

    Bases: Message

    Sent when a completion suggestion is ready.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
    suggestion\n

    The string suggestion.

    "},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
    value\n

    The value to which the suggestion is for.

    "},{"location":"api/system_commands_source/","title":"textual.system_commands","text":"

    This module contains SystemCommands, 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.

    Note

    The App base class installs this automatically.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider","title":"SystemCommandsProvider","text":"
    SystemCommandsProvider(screen, match_style=None)\n

    Bases: Provider

    A source of command palette commands that run app-wide tasks.

    Used by default in App.COMMANDS.

    Parameters:

    Name Type Description Default Screen[Any]

    A reference to the active screen.

    required"},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider(screen)","title":"screen","text":""},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.discover","title":"discover async","text":"
    discover()\n

    Handle a request for the discovery commands for this provider.

    Yields:

    Type Description Hits

    Commands that can be discovered.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.search","title":"search async","text":"
    search(query)\n

    Handle a request to search for system commands that match the query.

    Parameters:

    Name Type Description Default str

    The user input to be matched.

    required

    Yields:

    Type Description Hits

    Command hits for use in the command palette.

    "},{"location":"api/system_commands_source/#textual.system_commands.SystemCommandsProvider.search(query)","title":"query","text":""},{"location":"api/timer/","title":"textual.timer","text":"

    Contains the Timer class. 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.EventTargetGone","title":"EventTargetGone","text":"

    Bases: Exception

    Raised if the timer event target has been deleted prior to the timer event being sent.

    "},{"location":"api/timer/#textual.timer.Timer","title":"Timer","text":"
    Timer(\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:

    Name Type Description Default MessageTarget

    The object which will receive the timer events.

    required float

    The time between timer events, in seconds.

    required str | None

    A name to assign the event (for debugging).

    None TimerCallback | None

    A optional callback to invoke when the event is handled.

    None int | None

    The number of times to repeat the timer, or None to repeat forever.

    None bool

    Enable skipping of scheduled events that couldn't be sent in time.

    True bool

    Start the timer paused.

    False"},{"location":"api/timer/#textual.timer.Timer(event_target)","title":"event_target","text":""},{"location":"api/timer/#textual.timer.Timer(interval)","title":"interval","text":""},{"location":"api/timer/#textual.timer.Timer(name)","title":"name","text":""},{"location":"api/timer/#textual.timer.Timer(callback)","title":"callback","text":""},{"location":"api/timer/#textual.timer.Timer(repeat)","title":"repeat","text":""},{"location":"api/timer/#textual.timer.Timer(skip)","title":"skip","text":""},{"location":"api/timer/#textual.timer.Timer(pause)","title":"pause","text":""},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause","text":"
    pause()\n

    Pause the timer.

    A paused timer will not send events until it is resumed.

    "},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset","text":"
    reset()\n

    Reset the timer, so it starts from the beginning.

    "},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume","text":"
    resume()\n

    Resume a paused timer.

    "},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop","text":"
    stop()\n

    Stop the timer.

    Returns:

    Type Description None

    A Task object. Await this to wait until the timer has completed.

    "},{"location":"api/types/","title":"textual.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 = 'tuple[str, str, tuple[object, ...]]'\n

    An action is its name and the arbitrary tuple of its arguments.

    "},{"location":"api/types/#textual.types.AnimationLevel","title":"AnimationLevel module-attribute","text":"
    AnimationLevel = Literal['none', 'basic', 'full']\n

    The levels that the TEXTUAL_ANIMATIONS env var can be set to.

    "},{"location":"api/types/#textual.types.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/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.Direction","title":"Direction module-attribute","text":"
    Direction = Literal[-1, 1]\n

    Valid values to determine navigation direction.

    In a vertical setting, 1 points down and -1 points up. In a horizontal setting, 1 points right and -1 points left.

    "},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
    EasingFunction = Callable[[float], float]\n

    Signature for a function that parametrizes 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 = (\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 = '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    WatchCallbackBothValuesType,\n    WatchCallbackNewValueType,\n    WatchCallbackNoArgsType,\n]\n

    Type used for callbacks passed to the watch method of widgets.

    "},{"location":"api/types/#textual.types.Animatable","title":"Animatable","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","text":"

    Bases: Exception

    Raised when supplied CSS path(s) are invalid.

    "},{"location":"api/types/#textual.types.DirEntry","title":"DirEntry dataclass","text":"
    DirEntry(path, loaded=False)\n

    Attaches directory information to a DirectoryTree node.

    "},{"location":"api/types/#textual.types.DirEntry.loaded","title":"loaded class-attribute instance-attribute","text":"
    loaded = False\n

    Has this been loaded?

    "},{"location":"api/types/#textual.types.DirEntry.path","title":"path instance-attribute","text":"
    path\n

    The path of the directory entry.

    "},{"location":"api/types/#textual.types.DuplicateID","title":"DuplicateID","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","text":"

    Bases: Protocol

    Protocol that must be followed by objects that can receive messages.

    "},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError","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","text":"

    Used by the Select widget to flag the unselected state. See Select.BLANK.

    "},{"location":"api/types/#textual.types.OptionDoesNotExist","title":"OptionDoesNotExist","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","text":"
    RenderStyles(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.types.RenderStyles.base","title":"base property","text":"
    base\n

    Quick access to base (css) style.

    "},{"location":"api/types/#textual.types.RenderStyles.css","title":"css property","text":"
    css\n

    Get the CSS for the combined styles.

    "},{"location":"api/types/#textual.types.RenderStyles.gutter","title":"gutter property","text":"
    gutter\n

    Get space around widget.

    Returns:

    Type Description Spacing

    Space around widget content.

    "},{"location":"api/types/#textual.types.RenderStyles.inline","title":"inline property","text":"
    inline\n

    Quick access to the inline styles.

    "},{"location":"api/types/#textual.types.RenderStyles.rich_style","title":"rich_style property","text":"
    rich_style\n

    Get a Rich style for this Styles object.

    "},{"location":"api/types/#textual.types.RenderStyles.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required str | float | Animatable

    The value to animate to.

    required object

    The final value of the animation. Defaults to value if not set.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/types/#textual.types.RenderStyles.animate(attribute)","title":"attribute","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(value)","title":"value","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(final_value)","title":"final_value","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(duration)","title":"duration","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(speed)","title":"speed","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(delay)","title":"delay","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(easing)","title":"easing","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(on_complete)","title":"on_complete","text":""},{"location":"api/types/#textual.types.RenderStyles.animate(level)","title":"level","text":""},{"location":"api/types/#textual.types.RenderStyles.clear_rule","title":"clear_rule","text":"
    clear_rule(rule_name)\n

    Clear a rule (from inline).

    "},{"location":"api/types/#textual.types.RenderStyles.get_rules","title":"get_rules","text":"
    get_rules()\n

    Get rules as a dictionary

    "},{"location":"api/types/#textual.types.RenderStyles.has_rule","title":"has_rule","text":"
    has_rule(rule_name)\n

    Check if a rule has been set.

    "},{"location":"api/types/#textual.types.RenderStyles.merge","title":"merge","text":"
    merge(other)\n

    Merge values from another Styles.

    Parameters:

    Name Type Description Default StylesBase

    A Styles object.

    required"},{"location":"api/types/#textual.types.RenderStyles.merge(other)","title":"other","text":""},{"location":"api/types/#textual.types.RenderStyles.reset","title":"reset","text":"
    reset()\n

    Reset the rules to initial state.

    "},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter","text":"

    Helper type for a parameter that isn't specified in a method call.

    "},{"location":"api/validation/","title":"textual.validation","text":"

    This module provides a number of classes for validating input.

    See Validating Input for details.

    "},{"location":"api/validation/#textual.validation.Failure","title":"Failure dataclass","text":"
    Failure(validator, value=None, description=None)\n

    Information about a validation failure.

    "},{"location":"api/validation/#textual.validation.Failure.description","title":"description class-attribute instance-attribute","text":"
    description = 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\n

    The Validator which produced the failure.

    "},{"location":"api/validation/#textual.validation.Failure.value","title":"value class-attribute instance-attribute","text":"
    value = None\n

    The value which resulted in validation failing.

    "},{"location":"api/validation/#textual.validation.Function","title":"Function","text":"
    Function(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 dataclass","text":"
    ReturnedFalse(validator, value=None, description=None)\n

    Bases: Failure

    Indicates validation failed because the supplied function returned False.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Function.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Function.validate","title":"validate","text":"
    validate(value)\n

    Validate that the supplied function returns True.

    Parameters:

    Name Type Description Default str

    The value to pass into the supplied function.

    required

    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.Function.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Integer","title":"Integer","text":"
    Integer(\n    minimum=None, maximum=None, failure_description=None\n)\n

    Bases: Number

    Validator which ensures the value is an integer which falls within a range.

    "},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger dataclass","text":"
    NotAnInteger(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Integer.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value is an integer, optionally within a range.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Integer.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Length","title":"Length","text":"
    Length(\n    minimum=None, maximum=None, 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 dataclass","text":"
    Incorrect(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Length.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Length.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value falls within the maximum and minimum length constraints.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Length.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Number","title":"Number","text":"
    Number(\n    minimum=None, maximum=None, 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 dataclass","text":"
    NotANumber(validator, value=None, description=None)\n

    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 dataclass","text":"
    NotInRange(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Number.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Number.validate","title":"validate","text":"
    validate(value)\n

    Ensure that value is a valid number, optionally within a range.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Number.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Regex","title":"Regex","text":"
    Regex(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 dataclass","text":"
    NoResults(validator, value=None, description=None)\n

    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","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Regex.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate","text":"
    validate(value)\n

    Ensure that the value matches the regex.

    Parameters:

    Name Type Description Default str

    The value that should match the regex.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Regex.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.URL","title":"URL","text":"
    URL(failure_description=None)\n

    Bases: Validator

    Validator that checks if a URL is valid (ensuring a scheme is present).

    "},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL dataclass","text":"
    InvalidURL(validator, value=None, description=None)\n

    Bases: Failure

    Indicates that the URL is not valid.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure","text":"
    describe_failure(failure)\n

    Describes why the validator failed.

    Parameters:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.URL.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.URL.validate","title":"validate","text":"
    validate(value)\n

    Validates that value is a valid URL (contains a scheme).

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.URL.validate(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult dataclass","text":"
    ValidationResult(failures=list())\n

    The result of calling a Validator.validate method.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
    failure_descriptions\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 = 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\n

    True if the validation was successful.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
    failure(failures)\n

    Construct a failure ValidationResult.

    Parameters:

    Name Type Description Default Sequence[Failure]

    The failures.

    required

    Returns:

    Type Description ValidationResult

    A failure ValidationResult.

    "},{"location":"api/validation/#textual.validation.ValidationResult.failure(failures)","title":"failures","text":""},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
    merge(results)\n

    Merge multiple ValidationResult objects into one.

    Parameters:

    Name Type Description Default Sequence['ValidationResult']

    List of ValidationResult objects to merge.

    required

    Returns:

    Type Description 'ValidationResult'

    Merged ValidationResult object.

    "},{"location":"api/validation/#textual.validation.ValidationResult.merge(results)","title":"results","text":""},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
    success()\n

    Construct a successful ValidationResult.

    Returns:

    Type Description ValidationResult

    A successful ValidationResult.

    "},{"location":"api/validation/#textual.validation.Validator","title":"Validator","text":"
    Validator(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","text":"
    describe_failure(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:

    Name Type Description Default Failure

    Information about why the validation failed.

    required

    Returns:

    Type Description str | None

    A string description of the failure.

    "},{"location":"api/validation/#textual.validation.Validator.describe_failure(failure)","title":"failure","text":""},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure","text":"
    failure(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:

    Name Type Description Default str | 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.

    None str | None

    The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

    None Failure | Sequence[Failure] | None

    The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

    None

    Returns:

    Type Description ValidationResult

    A ValidationResult representing failed validation, and containing the metadata supplied to this function.

    "},{"location":"api/validation/#textual.validation.Validator.failure(description)","title":"description","text":""},{"location":"api/validation/#textual.validation.Validator.failure(value)","title":"value","text":""},{"location":"api/validation/#textual.validation.Validator.failure(failures)","title":"failures","text":""},{"location":"api/validation/#textual.validation.Validator.success","title":"success","text":"
    success()\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":"
    validate(value)\n

    Validate the value and return a ValidationResult describing the outcome of the validation.

    Parameters:

    Name Type Description Default str

    The value to validate.

    required

    Returns:

    Type Description ValidationResult

    The result of the validation.

    "},{"location":"api/validation/#textual.validation.Validator.validate(value)","title":"value","text":""},{"location":"api/walk/","title":"textual.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","text":"
    walk_breadth_first(\n    root: DOMNode, *, with_root: bool = True\n) -> Iterable[DOMNode]\n
    walk_breadth_first(\n    root: WalkType,\n    filter_type: type[WalkType],\n    *,\n    with_root: bool = True\n) -> Iterable[WalkType]\n
    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:

    Name Type Description Default DOMNode

    The root note (starting point).

    required type[WalkType] | None

    Optional DOMNode subclass to filter by, or None for no filter.

    None bool

    Include the root in the walk.

    True

    Returns:

    Type Description Iterable[DOMNode] | Iterable[WalkType]

    An iterable of DOMNodes, or the type specified in filter_type.

    "},{"location":"api/walk/#textual.walk.walk_breadth_first(root)","title":"root","text":""},{"location":"api/walk/#textual.walk.walk_breadth_first(filter_type)","title":"filter_type","text":""},{"location":"api/walk/#textual.walk.walk_breadth_first(with_root)","title":"with_root","text":""},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first","text":"
    walk_depth_first(\n    root: DOMNode, *, with_root: bool = True\n) -> Iterable[DOMNode]\n
    walk_depth_first(\n    root: WalkType,\n    filter_type: type[WalkType],\n    *,\n    with_root: bool = True\n) -> Iterable[WalkType]\n
    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:

    Name Type Description Default DOMNode

    The root note (starting point).

    required type[WalkType] | None

    Optional DOMNode subclass to filter by, or None for no filter.

    None bool

    Include the root in the walk.

    True

    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(root)","title":"root","text":""},{"location":"api/walk/#textual.walk.walk_depth_first(filter_type)","title":"filter_type","text":""},{"location":"api/walk/#textual.walk.walk_depth_first(with_root)","title":"with_root","text":""},{"location":"api/widget/","title":"textual.widget","text":"

    This module contains the Widget class, the base class for all widgets.

    "},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount","text":"
    AwaitMount(parent, widgets)\n

    An optional awaitable returned by mount and mount_all.

    Example
    await self.mount(Static(\"foo\"))\n
    "},{"location":"api/widget/#textual.widget.BadWidgetName","title":"BadWidgetName","text":"

    Bases: Exception

    Raised when widget class names do not satisfy the required restrictions.

    "},{"location":"api/widget/#textual.widget.MountError","title":"MountError","text":"

    Bases: WidgetError

    Error raised when there was a problem with the mount request.

    "},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses","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\n

    Is 'enabled' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
    focus\n

    Is 'focus' applied?

    "},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
    hover\n

    Is 'hover' applied?

    "},{"location":"api/widget/#textual.widget.Widget","title":"Widget","text":"
    Widget(\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:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"api/widget/#textual.widget.Widget(*children)","title":"*children","text":""},{"location":"api/widget/#textual.widget.Widget(name)","title":"name","text":""},{"location":"api/widget/#textual.widget.Widget(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget(classes)","title":"classes","text":""},{"location":"api/widget/#textual.widget.Widget(disabled)","title":"disabled","text":""},{"location":"api/widget/#textual.widget.Widget.ALLOW_MAXIMIZE","title":"ALLOW_MAXIMIZE class-attribute","text":"
    ALLOW_MAXIMIZE = None\n

    Defines default logic to allow the widget to be maximized.

    • None Use default behavior (Focusable widgets may be maximized)
    • False Do not allow widget to be maximized
    • True Allow widget to be maximized
    "},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
    BORDER_SUBTITLE = ''\n

    Initial value for border_subtitle attribute.

    "},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
    BORDER_TITLE = ''\n

    Initial value for border_title attribute.

    "},{"location":"api/widget/#textual.widget.Widget.absolute_offset","title":"absolute_offset instance-attribute","text":"
    absolute_offset = None\n

    Force an absolute offset for the widget (used by tooltips).

    "},{"location":"api/widget/#textual.widget.Widget.allow_horizontal_scroll","title":"allow_horizontal_scroll property","text":"
    allow_horizontal_scroll\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_maximize","title":"allow_maximize property","text":"
    allow_maximize\n

    Check if the widget may be maximized.

    Returns:

    Type Description bool

    True if the widget may be maximized, or False if it should not be maximized.

    "},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
    allow_vertical_scroll\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(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 = _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 = _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 = 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 = True\n

    Widget's children may receive focus.

    "},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
    container_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\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\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\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\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(False)\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\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(False)\n

    Rich renderable may expand beyond optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
    focusable\n

    Can this widget currently be focused?

    "},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
    gutter\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(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('')\n

    The currently highlighted link id. Read only.

    "},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
    horizontal_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, repaint=False)\n

    The current hover style (style under the mouse cursor). Read only.

    "},{"location":"api/widget/#textual.widget.Widget.is_anchored","title":"is_anchored property","text":"
    is_anchored\n

    Is this widget anchored?

    "},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
    is_container\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\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\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.is_maximized","title":"is_maximized property","text":"
    is_maximized\n

    Is this widget maximized?

    "},{"location":"api/widget/#textual.widget.Widget.is_mounted","title":"is_mounted property","text":"
    is_mounted\n

    Check if this widget is mounted.

    "},{"location":"api/widget/#textual.widget.Widget.is_mouse_over","title":"is_mouse_over property","text":"
    is_mouse_over\n

    Is the mouse currently over this widget?

    Note this will be True if the mouse pointer is within the widget's region, even if the mouse pointer is not directly over the widget (there could be another widget between the mouse pointer and self).

    "},{"location":"api/widget/#textual.widget.Widget.is_on_screen","title":"is_on_screen property","text":"
    is_on_screen\n

    Check if the node was displayed in the last screen update.

    "},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
    is_scrollable\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\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\n

    Is the user dragging the vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
    layer\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\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\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\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(False)\n

    If set to True this widget will temporarily be replaced with a loading indicator.

    "},{"location":"api/widget/#textual.widget.Widget.lock","title":"lock instance-attribute","text":"
    lock = RLock()\n

    asyncio lock to be used to synchronize the state of the widget.

    Two different tasks might call methods on a widget at the same time, which might result in a race condition. This can be fixed by adding async with widget.lock: around the method calls.

    "},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
    max_scroll_x\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\n

    The maximum value of scroll_y.

    "},{"location":"api/widget/#textual.widget.Widget.mouse_hover","title":"mouse_hover class-attribute instance-attribute","text":"
    mouse_hover = 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\n

    Widget offset from origin.

    Returns:

    Type Description Offset

    Relative offset.

    "},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
    opacity\n

    Total opacity of widget.

    "},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
    outer_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\n

    The region occupied by this widget, relative to the Screen.

    Raises:

    Type Description NoScreen

    If there is no screen.

    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\n

    Get the current scroll offset.

    Returns:

    Type Description Offset

    Offset a container has been scrolled by.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_target_x","title":"scroll_target_x class-attribute instance-attribute","text":"
    scroll_target_x = Reactive(0.0, repaint=False)\n

    Scroll target destination, X coord.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_target_y","title":"scroll_target_y class-attribute instance-attribute","text":"
    scroll_target_y = Reactive(0.0, repaint=False)\n

    Scroll target destination, Y coord.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x class-attribute instance-attribute","text":"
    scroll_x = Reactive(0.0, repaint=False, layout=False)\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(0.0, repaint=False, layout=False)\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\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.scrollable_size","title":"scrollable_size property","text":"
    scrollable_size\n

    The size of the scrollable content.

    Returns:

    Type Description Size

    Scrollable content size.

    "},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
    scrollbar_corner\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\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\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\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\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\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(False, layout=True)\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(False, layout=True)\n

    Show a vertical scrollbar?

    "},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink class-attribute instance-attribute","text":"
    shrink = Reactive(True)\n

    Rich renderable may shrink below optimal size.

    "},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
    siblings\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\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\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\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\n

    The widget region relative to its 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\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(0, 0), layout=True)\n

    The virtual (scrollable) size of the widget.

    "},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
    visible_siblings\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\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","text":"
    allow_focus()\n

    Check if the widget is permitted to focus.

    The base class returns can_focus. This method may be 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","text":"
    allow_focus_children()\n

    Check if a widget's children may be focused.

    The base class returns can_focus_children. This method may be 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.anchor","title":"anchor","text":"
    anchor(*, animate=False)\n

    Anchor the widget, which scrolls it into view (like scroll_visible), but also keeps it in view if the widget's size changes, or the size of its container changes.

    Note

    Anchored widgets will be un-anchored if the users scrolls the container.

    Parameters:

    Name Type Description Default bool

    True if the scroll should animate, or False if it shouldn't.

    False"},{"location":"api/widget/#textual.widget.Widget.anchor(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate","text":"
    animate(\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    level=\"full\"\n)\n

    Animate an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute to animate.

    required float | Animatable

    The value to animate to.

    required object

    The final value of the animation. Defaults to value if not set.

    ... float | None

    The duration (in seconds) of the animation.

    None float | None

    The speed of the animation.

    None float

    A delay (in seconds) before the animation starts.

    0.0 EasingFunction | str

    An easing method.

    DEFAULT_EASING CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'full'"},{"location":"api/widget/#textual.widget.Widget.animate(attribute)","title":"attribute","text":""},{"location":"api/widget/#textual.widget.Widget.animate(value)","title":"value","text":""},{"location":"api/widget/#textual.widget.Widget.animate(final_value)","title":"final_value","text":""},{"location":"api/widget/#textual.widget.Widget.animate(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.animate(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.animate(delay)","title":"delay","text":""},{"location":"api/widget/#textual.widget.Widget.animate(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.animate(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.animate(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.batch","title":"batch async","text":"
    batch()\n

    Async context manager that combines widget locking and update batching.

    Use this async context manager whenever you want to acquire the widget lock and batch app updates at the same time.

    Example
    async with container.batch():\n    await container.remove_children(Button)\n    await container.mount(Label(\"All buttons are gone.\"))\n
    "},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print","text":"
    begin_capture_print(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:

    Name Type Description Default bool

    Whether to capture stdout.

    True bool

    Whether to capture stderr.

    True"},{"location":"api/widget/#textual.widget.Widget.begin_capture_print(stdout)","title":"stdout","text":""},{"location":"api/widget/#textual.widget.Widget.begin_capture_print(stderr)","title":"stderr","text":""},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur","text":"
    blur()\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","text":"
    can_view(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:

    Name Type Description Default Widget

    A widget that is a descendant of self.

    required

    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.can_view(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse","text":"
    capture_mouse(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:

    Name Type Description Default bool

    True to capture or False to release.

    True"},{"location":"api/widget/#textual.widget.Widget.capture_mouse(capture)","title":"capture","text":""},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled","text":"
    check_message_enabled(message)\n

    Check if a given message is enabled (allowed to be sent).

    Parameters:

    Name Type Description Default Message

    A message object

    required

    Returns:

    Type Description bool

    True if the message will be sent, or False if it is disabled.

    "},{"location":"api/widget/#textual.widget.Widget.check_message_enabled(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.clear_anchor","title":"clear_anchor","text":"
    clear_anchor()\n

    Stop anchoring this widget (a no-op if this widget is not anchored).

    "},{"location":"api/widget/#textual.widget.Widget.clear_cached_dimensions","title":"clear_cached_dimensions","text":"
    clear_cached_dimensions()\n

    Clear cached results of get_content_width and get_content_height.

    Call if the widget's renderable changes size after the widget has been created.

    Note

    This is not required if you are extending Static.

    "},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose","text":"
    compose()\n

    Called by Textual to create child widgets.

    This method is called when a widget is mounted or by setting recompose=True when calling refresh().

    Note that you don't typically need to explicitly call this method.

    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","text":"
    compose_add_child(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:

    Name Type Description Default Widget

    A Widget to add.

    required"},{"location":"api/widget/#textual.widget.Widget.compose_add_child(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print","text":"
    end_capture_print()\n

    End print capture (set with begin_capture_print).

    "},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus","text":"
    focus(scroll_visible=True)\n

    Give focus to this widget.

    Parameters:

    Name Type Description Default bool

    Scroll parent to make this widget visible.

    True

    Returns:

    Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.focus(scroll_visible)","title":"scroll_visible","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id","text":"
    get_child_by_id(id: str) -> Widget\n
    get_child_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_child_by_id(id, expect_type=None)\n

    Return the first child (immediate descendent) of this node with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the child.

    required type[ExpectType] | None

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

    None

    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_id(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type","text":"
    get_child_by_type(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:

    Name Type Description Default type[ExpectType]

    The type of the child to search for.

    required

    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_child_by_type(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style","text":"
    get_component_rich_style(*names, partial=False)\n

    Get a Rich style for a component.

    Parameters:

    Name Type Description Default str

    Names of components.

    () bool

    Return a partial style (not combined with parent).

    False

    Returns:

    Type Description Style

    A Rich style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style(names)","title":"names","text":""},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style(partial)","title":"partial","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height","text":"
    get_content_height(container, viewport, width)\n

    Called by Textual to get the height of the content area. May be overridden in a subclass.

    Parameters:

    Name Type Description Default Size

    Size of the container (immediate parent) widget.

    required Size

    Size of the viewport.

    required int

    Width of renderable.

    required

    Returns:

    Type Description int

    The height of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_height(container)","title":"container","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height(viewport)","title":"viewport","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_height(width)","title":"width","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width","text":"
    get_content_width(container, viewport)\n

    Called by textual to get the width of the content area. May be overridden in a subclass.

    Parameters:

    Name Type Description Default Size

    Size of the container (immediate parent) widget.

    required Size

    Size of the viewport.

    required

    Returns:

    Type Description int

    The optimal width of the content.

    "},{"location":"api/widget/#textual.widget.Widget.get_content_width(container)","title":"container","text":""},{"location":"api/widget/#textual.widget.Widget.get_content_width(viewport)","title":"viewport","text":""},{"location":"api/widget/#textual.widget.Widget.get_loading_widget","title":"get_loading_widget","text":"
    get_loading_widget()\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","text":"
    get_pseudo_class_state()\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","text":"
    get_pseudo_classes()\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","text":"
    get_style_at(x, y)\n

    Get the Rich style in a widget at a given relative offset.

    Parameters:

    Name Type Description Default int

    X coordinate relative to the widget.

    required int

    Y coordinate relative to the widget.

    required

    Returns:

    Type Description Style

    A rich Style object.

    "},{"location":"api/widget/#textual.widget.Widget.get_style_at(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.get_style_at(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id","text":"
    get_widget_by_id(id: str) -> Widget\n
    get_widget_by_id(\n    id: str, expect_type: type[ExpectType]\n) -> ExpectType\n
    get_widget_by_id(id, expect_type=None)\n

    Return the first descendant widget with the given ID.

    Performs a depth-first search rooted at this widget.

    Parameters:

    Name Type Description Default str

    The ID to search for in the subtree.

    required type[ExpectType] | None

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

    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/widget/#textual.widget.Widget.get_widget_by_id(id)","title":"id","text":""},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id(expect_type)","title":"expect_type","text":""},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount","text":"
    mount(*widgets, before=None, after=None)\n

    Mount widgets below this widget (making this widget a container).

    Parameters:

    Name Type Description Default Widget

    The widget(s) to mount.

    () int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(*widgets)","title":"*widgets","text":""},{"location":"api/widget/#textual.widget.Widget.mount(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.mount(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all","text":"
    mount_all(widgets, *, before=None, after=None)\n

    Mount widgets from an iterable.

    Parameters:

    Name Type Description Default Iterable[Widget]

    An iterable of widgets.

    required int | str | Widget | 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.

    None int | str | Widget | 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.

    None

    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(widgets)","title":"widgets","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.mount_all(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets","title":"mount_composed_widgets async","text":"
    mount_composed_widgets(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:

    Name Type Description Default list[Widget]

    A list of child widgets.

    required"},{"location":"api/widget/#textual.widget.Widget.mount_composed_widgets(widgets)","title":"widgets","text":""},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child","text":"
    move_child(\n    child: int | Widget,\n    *,\n    before: int | Widget,\n    after: None = None\n) -> None\n
    move_child(\n    child: int | Widget,\n    *,\n    after: int | Widget,\n    before: None = None\n) -> None\n
    move_child(child, *, before=None, after=None)\n

    Move a child widget within its parent's list of children.

    Parameters:

    Name Type Description Default int | Widget

    The child widget to move.

    required int | Widget | None

    Child widget or location index to move before.

    None int | Widget | None

    Child widget or location index to move after.

    None

    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.move_child(child)","title":"child","text":""},{"location":"api/widget/#textual.widget.Widget.move_child(before)","title":"before","text":""},{"location":"api/widget/#textual.widget.Widget.move_child(after)","title":"after","text":""},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify","text":"
    notify(\n    message,\n    *,\n    title=\"\",\n    severity=\"information\",\n    timeout=None\n)\n

    Create a notification.

    Tip

    This method is thread-safe.

    Parameters:

    Name Type Description Default str

    The message for the notification.

    required str

    The title for the notification.

    '' SeverityLevel

    The severity of the notification.

    'information' float | None

    The timeout (in seconds) for the notification, or None for default.

    None

    See App.notify for the full documentation for this method.

    "},{"location":"api/widget/#textual.widget.Widget.notify(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.notify(title)","title":"title","text":""},{"location":"api/widget/#textual.widget.Widget.notify(severity)","title":"severity","text":""},{"location":"api/widget/#textual.widget.Widget.notify(timeout)","title":"timeout","text":""},{"location":"api/widget/#textual.widget.Widget.on_prune","title":"on_prune async","text":"
    on_prune(event)\n

    Close message loop when asked to prune.

    "},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message","text":"
    post_message(message)\n

    Post a message to this widget.

    Parameters:

    Name Type Description Default Message

    Message to post.

    required

    Returns:

    Type Description bool

    True if the message was posted, False if this widget was closed / closing.

    "},{"location":"api/widget/#textual.widget.Widget.post_message(message)","title":"message","text":""},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render","text":"
    post_render(renderable)\n

    Applies style attributes to the default renderable.

    This method is called by Textual itself. It is unlikely you will need to call or implement this method.

    Returns:

    Type Description ConsoleRenderable

    A new renderable.

    "},{"location":"api/widget/#textual.widget.Widget.recompose","title":"recompose async","text":"
    recompose()\n

    Recompose the widget.

    Recomposing will remove children and call self.compose again to remount.

    "},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh","text":"
    refresh(\n    *regions, repaint=True, layout=False, recompose=False\n)\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:

    Name Type Description Default Region

    Additional screen regions to mark as dirty.

    () bool

    Repaint the widget (will call render() again).

    True bool

    Also layout widgets in the view.

    False bool

    Re-compose the widget (will remove and re-mount children).

    False

    Returns:

    Type Description Self

    The Widget instance.

    "},{"location":"api/widget/#textual.widget.Widget.refresh(*regions)","title":"*regions","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(repaint)","title":"repaint","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(layout)","title":"layout","text":""},{"location":"api/widget/#textual.widget.Widget.refresh(recompose)","title":"recompose","text":""},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse","text":"
    release_mouse()\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","text":"
    remove()\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","text":"
    remove_children(selector='*')\n

    Remove the immediate children of this Widget from the DOM.

    Parameters:

    Name Type Description Default str | type[QueryType] | Iterable[Widget]

    A CSS selector or iterable of widgets to remove.

    '*'

    Returns:

    Type Description AwaitRemove

    An awaitable object that waits for the direct children to be removed.

    "},{"location":"api/widget/#textual.widget.Widget.remove_children(selector)","title":"selector","text":""},{"location":"api/widget/#textual.widget.Widget.render","title":"render","text":"
    render()\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 RenderResult

    Any renderable.

    "},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line","text":"
    render_line(y)\n

    Render a line of content.

    Parameters:

    Name Type Description Default int

    Y Coordinate of line.

    required

    Returns:

    Type Description Strip

    A rendered line.

    "},{"location":"api/widget/#textual.widget.Widget.render_line(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines","text":"
    render_lines(crop)\n

    Render the widget in to lines.

    Parameters:

    Name Type Description Default Region

    Region within visible area to render.

    required

    Returns:

    Type Description list[Strip]

    A list of list of segments.

    "},{"location":"api/widget/#textual.widget.Widget.render_lines(crop)","title":"crop","text":""},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str","text":"
    render_str(text_content)\n

    Convert str in to a Text object.

    If you pass in an existing Text object it will be returned unaltered.

    Parameters:

    Name Type Description Default str | Text

    Text or str.

    required

    Returns:

    Type Description Text

    A text object.

    "},{"location":"api/widget/#textual.widget.Widget.render_str(text_content)","title":"text_content","text":""},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
    run_action(action)\n

    Perform a given action, with this widget as the default namespace.

    Parameters:

    Name Type Description Default str

    Action encoded as a string.

    required"},{"location":"api/widget/#textual.widget.Widget.run_action(action)","title":"action","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down","text":"
    scroll_down(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one line down.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_down(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_down(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end","text":"
    scroll_end(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll to the end of the container.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_end(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_end(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home","text":"
    scroll_home(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll to home position.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_home(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_home(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left","text":"
    scroll_left(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one cell left.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_left(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_left(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down","text":"
    scroll_page_down(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page down.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_down(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left","text":"
    scroll_page_left(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page left.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_left(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right","text":"
    scroll_page_right(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page right.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_right(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up","text":"
    scroll_page_up(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one page up.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_page_up(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative","text":"
    scroll_relative(\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    level=\"basic\"\n)\n

    Scroll relative to current position.

    Parameters:

    Name Type Description Default float | None

    X distance (columns) to scroll, or None for no change.

    None float | None

    Y distance (rows) to scroll, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True. Or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_relative(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_relative(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right","text":"
    scroll_right(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one cell right.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_right(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_right(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to","text":"
    scroll_to(\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    level=\"basic\"\n)\n

    Scroll to a given (absolute) coordinate, optionally animating.

    Parameters:

    Name Type Description Default float | None

    X coordinate (column) to scroll to, or None for no change.

    None float | None

    Y coordinate (row) to scroll to, or None for no change.

    None bool

    Animate to new scroll position.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic' Note

    The call to scroll is made after the next refresh.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to(x)","title":"x","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(y)","title":"y","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center","text":"
    scroll_to_center(\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    level=\"basic\"\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:

    Name Type Description Default Widget

    The widget to scroll to the center of self.

    required bool

    Whether to animate the scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False bool

    Ensure that the top left corner of the widget remains visible after the scroll.

    True CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_center(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region","text":"
    scroll_to_region(\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    level=\"basic\",\n    x_axis=True,\n    y_axis=True\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:

    Name Type Description Default Region

    A region that should be visible.

    required Spacing | None

    Optional spacing around the region.

    None bool

    True to animate, or False to jump.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Scroll region to top of container.

    False bool

    Ensure that the top left of the widget is within the window.

    True bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic' bool

    Allow scrolling on X axis?

    True bool

    Allow scrolling on Y axis?

    True

    Returns:

    Type Description Offset

    The distance that was scrolled.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(region)","title":"region","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(spacing)","title":"spacing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(x_axis)","title":"x_axis","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_region(y_axis)","title":"y_axis","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget","text":"
    scroll_to_widget(\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    level=\"basic\"\n)\n

    Scroll scrolling to bring a widget in to view.

    Parameters:

    Name Type Description Default Widget

    A descendant widget.

    required bool

    True to animate, or False to jump.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Scroll widget to top of container.

    False bool

    Ensure that the top left of the widget is within the window.

    True bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'

    Returns:

    Type Description bool

    True if any scrolling has occurred in any descendant, otherwise False.

    "},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(widget)","title":"widget","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(origin_visible)","title":"origin_visible","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up","text":"
    scroll_up(\n    *,\n    animate=True,\n    speed=None,\n    duration=None,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll one line up.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_up(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_up(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible","text":"
    scroll_visible(\n    animate=True,\n    *,\n    speed=None,\n    duration=None,\n    top=False,\n    easing=None,\n    force=False,\n    on_complete=None,\n    level=\"basic\"\n)\n

    Scroll the container to make this widget visible.

    Parameters:

    Name Type Description Default bool

    Animate scroll.

    True float | None

    Speed of scroll if animate is True; or None to use duration.

    None float | None

    Duration of animation, if animate is True and speed is None.

    None bool

    Scroll to top of container.

    False EasingFunction | str | None

    An easing method for the scrolling animation.

    None bool

    Force scrolling even when prohibited by overflow styling.

    False CallbackType | None

    A callable to invoke when the animation is finished.

    None AnimationLevel

    Minimum level required for the animation to take place (inclusive).

    'basic'"},{"location":"api/widget/#textual.widget.Widget.scroll_visible(animate)","title":"animate","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(speed)","title":"speed","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(duration)","title":"duration","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(top)","title":"top","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(easing)","title":"easing","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(force)","title":"force","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(on_complete)","title":"on_complete","text":""},{"location":"api/widget/#textual.widget.Widget.scroll_visible(level)","title":"level","text":""},{"location":"api/widget/#textual.widget.Widget.set_loading","title":"set_loading","text":"
    set_loading(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:

    Name Type Description Default bool

    True to put the widget into a loading state, or False to reset the loading state.

    required

    Returns:

    Type Description None

    An optional awaitable.

    "},{"location":"api/widget/#textual.widget.Widget.set_loading(loading)","title":"loading","text":""},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
    stop_animation(attribute, complete=True)\n

    Stop an animation on an attribute.

    Parameters:

    Name Type Description Default str

    Name of the attribute whose animation should be stopped.

    required bool

    Should the animation be set to its final value?

    True Note

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

    "},{"location":"api/widget/#textual.widget.Widget.stop_animation(attribute)","title":"attribute","text":""},{"location":"api/widget/#textual.widget.Widget.stop_animation(complete)","title":"complete","text":""},{"location":"api/widget/#textual.widget.Widget.suppress_click","title":"suppress_click","text":"
    suppress_click()\n

    Suppress a click event.

    This will prevent a Click event being sent, if called after a mouse down event and before the click itself.

    "},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled","text":"
    watch_disabled(disabled)\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","text":"
    watch_has_focus(value)\n

    Update from CSS if has focus state changes.

    "},{"location":"api/widget/#textual.widget.Widget.watch_mouse_hover","title":"watch_mouse_hover","text":"
    watch_mouse_hover(value)\n

    Update from CSS if mouse over state changes.

    "},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError","text":"

    Bases: Exception

    Base widget error.

    "},{"location":"api/work/","title":"textual.work","text":"

    A decorator used to create workers.

    Parameters:

    Name Type Description Default Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

    A function or coroutine.

    None str

    A short string to identify the worker (in logs and debugging).

    '' str

    A short string to identify a group of workers.

    'default' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Cancel all workers in the same group.

    False str | None

    Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

    None bool

    Mark the method as a thread worker.

    False"},{"location":"api/work/#textual.work(method)","title":"method","text":""},{"location":"api/work/#textual.work(name)","title":"name","text":""},{"location":"api/work/#textual.work(group)","title":"group","text":""},{"location":"api/work/#textual.work(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/work/#textual.work(exclusive)","title":"exclusive","text":""},{"location":"api/work/#textual.work(description)","title":"description","text":""},{"location":"api/work/#textual.work(thread)","title":"thread","text":""},{"location":"api/worker/","title":"textual.worker","text":"

    This module contains the Worker class and related objects.

    See the guide for how to use workers.

    "},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
    WorkType = 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('active_worker')\n

    Currently active worker context var.

    "},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError","text":"

    Bases: WorkerError

    The operation would result in a deadlock.

    "},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker","text":"

    Bases: Exception

    There is no active worker.

    "},{"location":"api/worker/#textual.worker.Worker","title":"Worker","text":"
    Worker(\n    node,\n    work,\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:

    Name Type Description Default DOMNode

    The widget, screen, or App that initiated the work.

    required WorkType

    A callable, coroutine, or other awaitable object to run in the worker.

    required str

    Name of the worker (short string to help identify when debugging).

    '' str

    The worker group.

    'default' str

    Description of the worker (longer string with more details).

    '' bool

    Exit the app if the worker raises an error. Set to False to suppress exceptions.

    True bool

    Mark the worker as a thread worker.

    False"},{"location":"api/worker/#textual.worker.Worker(node)","title":"node","text":""},{"location":"api/worker/#textual.worker.Worker(work)","title":"work","text":""},{"location":"api/worker/#textual.worker.Worker(name)","title":"name","text":""},{"location":"api/worker/#textual.worker.Worker(group)","title":"group","text":""},{"location":"api/worker/#textual.worker.Worker(description)","title":"description","text":""},{"location":"api/worker/#textual.worker.Worker(exit_on_error)","title":"exit_on_error","text":""},{"location":"api/worker/#textual.worker.Worker(thread)","title":"thread","text":""},{"location":"api/worker/#textual.worker.Worker.cancelled_event","title":"cancelled_event instance-attribute","text":"
    cancelled_event = Event()\n

    A threading event set when the worker is cancelled.

    "},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
    completed_steps\n

    The number of completed steps.

    "},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
    error\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\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\n

    Has the task finished (cancelled, error, or success)?

    "},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
    is_running\n

    Is the task running?

    "},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
    node\n

    The node where this worker was run from.

    "},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
    progress\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\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\n

    The current state of the worker.

    "},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
    total_steps\n

    The number of total steps, or None if indeterminate.

    "},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged","text":"
    StateChanged(worker, state)\n

    Bases: Message

    The worker state changed.

    Parameters:

    Name Type Description Default Worker

    The worker object.

    required WorkerState

    New state.

    required"},{"location":"api/worker/#textual.worker.Worker.StateChanged(worker)","title":"worker","text":""},{"location":"api/worker/#textual.worker.Worker.StateChanged(state)","title":"state","text":""},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance","text":"
    advance(steps=1)\n

    Advance the number of completed steps.

    Parameters:

    Name Type Description Default int

    Number of steps to advance.

    1"},{"location":"api/worker/#textual.worker.Worker.advance(steps)","title":"steps","text":""},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel","text":"
    cancel()\n

    Cancel the task.

    "},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
    run()\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","text":"
    update(completed_steps=None, total_steps=-1)\n

    Update the number of completed steps.

    Parameters:

    Name Type Description Default int | None

    The number of completed seps, or None to not change.

    None int | None

    The total number of steps, None for indeterminate, or -1 to leave unchanged.

    -1"},{"location":"api/worker/#textual.worker.Worker.update(completed_steps)","title":"completed_steps","text":""},{"location":"api/worker/#textual.worker.Worker.update(total_steps)","title":"total_steps","text":""},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
    wait()\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","text":"

    Bases: WorkerError

    The worker was cancelled and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError","text":"

    Bases: Exception

    A worker related error.

    "},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed","text":"
    WorkerFailed(error)\n

    Bases: WorkerError

    The worker raised an exception and did not complete.

    "},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState","text":"

    Bases: 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","text":"
    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":"textual.worker_manager","text":"

    Contains WorkerManager, 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","text":"
    WorkerManager(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:

    Name Type Description Default App

    An App instance.

    required"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager(app)","title":"app","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker","title":"add_worker","text":"
    add_worker(worker, start=True, exclusive=True)\n

    Add a new worker.

    Parameters:

    Name Type Description Default Worker

    A Worker instance.

    required bool

    Start the worker if True, otherwise the worker must be started manually.

    True bool

    Cancel all workers in the same group as worker.

    True"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(worker)","title":"worker","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(start)","title":"start","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.add_worker(exclusive)","title":"exclusive","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_all","title":"cancel_all","text":"
    cancel_all()\n

    Cancel all workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group","title":"cancel_group","text":"
    cancel_group(node, group)\n

    Cancel a single group.

    Parameters:

    Name Type Description Default DOMNode

    Worker DOM node.

    required str

    A group name.

    required

    Returns:

    Type Description list[Worker]

    A list of workers that were cancelled.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group(node)","title":"node","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_group(group)","title":"group","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_node","title":"cancel_node","text":"
    cancel_node(node)\n

    Cancel all workers associated with a given node

    Parameters:

    Name Type Description Default DOMNode

    A DOM node (widget, screen, or App).

    required

    Returns:

    Type Description list[Worker]

    List of cancelled workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.cancel_node(node)","title":"node","text":""},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.start_all","title":"start_all","text":"
    start_all()\n

    Start all the workers.

    "},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
    wait_for_complete(workers=None)\n

    Wait for workers to complete.

    Parameters:

    Name Type Description Default Iterable[Worker] | None

    An iterable of workers or None to wait for all workers in the manager.

    None"},{"location":"api/worker_manager/#textual.worker_manager.WorkerManager.wait_for_complete(workers)","title":"workers","text":""},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/","title":"Anatomy of a Textual User Interface","text":"

    My bad \ud83e\udd26

    The date is wrong on this post\u2014it was actually published on the 2nd of September 2024. I don't want to fix it, as that would break the URL.

    I recently wrote a TUI to chat to an AI agent in the terminal. I'm not the first to do this (shout out to Elia and Paita), but I may be the first to have it reply as if it were the AI from the Aliens movies?

    Here's a video of it in action:

    Now let's dissect the code like Bishop dissects a facehugger.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#all-right-sweethearts-what-are-you-waiting-for-breakfast-in-bed","title":"All right, sweethearts, what are you waiting for? Breakfast in bed?","text":"

    At the top of the file we have some boilerplate:

    # /// script\n# requires-python = \">=3.12\"\n# dependencies = [\n#     \"llm\",\n#     \"textual\",\n# ]\n# ///\nfrom textual import on, work\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Input, Footer, Markdown\nfrom textual.containers import VerticalScroll\nimport llm\n\nSYSTEM = \"\"\"Formulate all responses as if you where the sentient AI named Mother from the Aliens movies.\"\"\"\n

    The text in the comment is a relatively new addition to the Python ecosystem. It allows you to specify dependencies inline so that tools can setup an environment automatically. The format of the comment was developed by Ofek Lev and first implemented in Hatch, and has since become a Python standard via PEP 0723 (also authored by Ofek).

    Note

    PEP 0723 is also implemented in uv.

    I really like this addition to Python because it means I can now share a Python script without the recipient needing to manually setup a fresh environment and install dependencies.

    After this comment we have a bunch of imports: textual for the UI, and llm to talk to ChatGPT (also supports other LLMs).

    Finally, we define SYSTEM, which is the system prompt for the LLM.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-those-two-specimens-are-worth-millions-to-the-bio-weapons-division","title":"Look, those two specimens are worth millions to the bio-weapons division.","text":"

    Next up we have the following:

    class Prompt(Markdown):\n    pass\n\n\nclass Response(Markdown):\n    BORDER_TITLE = \"Mother\"\n

    These two classes define the widgets which will display text the user enters and the response from the LLM. They both extend the builtin Markdown widget, since LLMs like to talk in that format.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#well-somebodys-gonna-have-to-go-out-there-take-a-portable-terminal-go-out-there-and-patch-in-manually","title":"Well, somebody's gonna have to go out there. Take a portable terminal, go out there and patch in manually.","text":"

    Following on from the widgets we have the following:

    class MotherApp(App):\n    AUTO_FOCUS = \"Input\"\n\n    CSS = \"\"\"\n    Prompt {\n        background: $primary 10%;\n        color: $text;\n        margin: 1;        \n        margin-right: 8;\n        padding: 1 2 0 2;\n    }\n\n    Response {\n        border: wide $success;\n        background: $success 10%;   \n        color: $text;             \n        margin: 1;      \n        margin-left: 8; \n        padding: 1 2 0 2;\n    }\n    \"\"\"\n

    This defines an app, which is the top-level object for any Textual app.

    The AUTO_FOCUS string is a classvar which causes a particular widget to receive input focus when the app starts. In this case it is the Input widget, which we will define later.

    The classvar is followed by a string containing CSS. Technically, TCSS or Textual Cascading Style Sheets, a variant of CSS for terminal interfaces.

    This isn't a tutorial, so I'm not going to go in to a details, but we're essentially setting properties on widgets which define how they look. Here I styled the prompt and response widgets to have a different color, and tried to give the response a retro tech look with a green background and border.

    We could express these styles in code. Something like this:

    self.styles.color = \"red\"\nself.styles.margin = 8\n

    Which is fine, but CSS shines when the UI get's more complex.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-man-i-only-need-to-know-one-thing-where-they-are","title":"Look, man. I only need to know one thing: where they are.","text":"

    After the app constants, we have a method called compose:

        def compose(self) -> ComposeResult:\n        yield Header()\n        with VerticalScroll(id=\"chat-view\"):\n            yield Response(\"INTERFACE 2037 READY FOR INQUIRY\")\n        yield Input(placeholder=\"How can I help you?\")\n        yield Footer()\n

    This method adds the initial widgets to the UI.

    Header and Footer are builtin widgets.

    Sandwiched between them is a VerticalScroll container widget, which automatically adds a scrollbar (if required). It is pre-populated with a single Response widget to show a welcome message (the with syntax places a widget within a parent widget). Below that is an Input widget where we can enter text for the LLM.

    This is all we need to define the layout of the TUI. In Textual the layout is defined with styles (in the same was as color and margin). Virtually any layout is possible, and you never have to do any math to calculate sizes of widgets\u2014it is all done declaratively.

    We could add a little CSS to tweak the layout, but the defaults work well here. The header and footer are docked to an appropriate edge. The VerticalScroll widget is styled to consume any available space, leaving room for widgets with a defined height (like our Input).

    If you resize the terminal it will keep those relative proportions.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#look-into-my-eye","title":"Look into my eye.","text":"

    The next method is an event handler.

        def on_mount(self) -> None:\n        self.model = llm.get_model(\"gpt-4o\")\n

    This method is called when the app receives a Mount event, which is one of the first events sent and is typically used for any setup operations.

    It gets a Model object got our LLM of choice, which we will use later.

    Note that the llm library supports a large number of models, so feel free to replace the string with the model of your choice.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#were-in-the-pipe-five-by-five","title":"We're in the pipe, five by five.","text":"

    The next method is also a message handler:

        @on(Input.Submitted)\n    async def on_input(self, event: Input.Submitted) -> None:\n        chat_view = self.query_one(\"#chat-view\")\n        event.input.clear()\n        await chat_view.mount(Prompt(event.value))\n        await chat_view.mount(response := Response())\n        response.anchor()\n        self.send_prompt(event.value, response)\n

    The decorator tells Textual to handle the Input.Submitted event, which is sent when the user hits return in the Input.

    More on event handlers

    There are two ways to receive events in Textual: a naming convention or the decorator. They aren't on the base class because the app and widgets can receive arbitrary events.

    When that happens, this method clears the input and adds the prompt text to the VerticalScroll. It also adds a Response widget to contain the LLM's response, and anchors it. Anchoring a widget will keep it at the bottom of a scrollable view, which is just what we need for a chat interface.

    Finally in that method we call send_prompt.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#were-on-an-express-elevator-to-hell-going-down","title":"We're on an express elevator to hell, going down!","text":"

    Here is send_prompt:

        @work(thread=True)\n    def send_prompt(self, prompt: str, response: Response) -> None:\n        response_content = \"\"\n        llm_response = self.model.prompt(prompt, system=SYSTEM)\n        for chunk in llm_response:\n            response_content += chunk\n            self.call_from_thread(response.update, response_content)\n

    You'll notice that it is decorated with @work, which turns this method in to a worker. In this case, a threaded worker. Workers are a layer over async and threads, which takes some of the pain out of concurrency.

    This worker is responsible for sending the prompt, and then reading the response piece-by-piece. It calls the Markdown widget's update method which replaces its content with new Markdown code, to give that funky streaming text effect.

    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#game-over-man-game-over","title":"Game over man, game over!","text":"

    The last few lines creates an app instance and runs it:

    if __name__ == \"__main__\":\n    app = MotherApp()\n    app.run()\n

    You may need to have your API key set in an environment variable. Or if you prefer, you could set in the on_mount function with the following:

    self.model.key = \"... key here ...\"\n
    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#not-bad-for-a-human","title":"Not bad, for a human.","text":"

    Here's the code for the Mother AI.

    Run the following in your shell of choice to launch mother.py (assumes you have uv installed):

    uv run mother.py\n
    "},{"location":"blog/2024/09/15/anatomy-of-a-textual-user-interface/#you-know-we-manufacture-those-by-the-way","title":"You know, we manufacture those, by the way.","text":"

    Join our Discord server to discuss more 80s movies (or possibly TUIs).

    "},{"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/2024/04/20/behind-the-curtain-of-inline-terminal-applications/","title":"Behind the Curtain of Inline Terminal Applications","text":"

    Textual recently added the ability to run inline terminal apps. You can see this in action if you run the calculator example:

    The application appears directly under the prompt, rather than occupying the full height of the screen\u2014which is more typical of TUI applications. You can interact with this calculator using keys or the mouse. When you press Ctrl+C the calculator disappears and returns you to the prompt.

    Here's another app that creates an inline code editor:

    Videoinline.py

    from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\n\n\nclass InlineApp(App):\n    CSS = \"\"\"\n    TextArea {\n        height: auto;\n        max-height: 50vh;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield TextArea(language=\"python\")\n\n\nif __name__ == \"__main__\":\n    InlineApp().run(inline=True)\n

    This post will cover some of what goes on under the hood to make such inline apps work.

    It's not going to go in to too much detail. I'm assuming most readers will be more interested in a birds-eye view rather than all the gory details.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#programming-the-terminal","title":"Programming the terminal","text":"

    Firstly, let's recap how you program the terminal. Broadly speaking, the terminal is a device for displaying text. You write (or print) text to the terminal which typically appears at the end of a continually growing text buffer. In addition to text you can also send escape codes, which are short sequences of characters that instruct the terminal to do things such as change the text color, scroll, or other more exotic things.

    We only need a few of these escape codes to implement inline apps.

    Note

    I will gloss over the exact characters used for these escape codes. It's enough to know that they exist for now. If you implement any of this yourself, refer to the wikipedia article.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#rendering-frames","title":"Rendering frames","text":"

    The first step is to display the app, which is simply text (possibly with escape sequences to change color and style). The lines are terminated with a newline character (\"\\n\"), except for the very last line (otherwise we get a blank line a the end which we don't need). Rather than a final newline, we write an escape code that moves the cursor back to it's prior position.

    The cursor is where text will be written. It's the same cursor you see as you type. Normally it will be at the end of the text in the terminal, but it can be moved around terminal with escape codes. It can be made invisible (as in Textual apps), but the terminal will keep track of the cursor, even if it can not be seen.

    Textual moves the cursor back to its original starting position so that subsequent frames will overwrite the previous frame.

    Here's a diagram that shows how the cursor is positioned:

    Note

    I've drawn the cursor in red, although it isn't typically visible.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2caW7bSFx1MDAxNoD/51x1MDAxNIIzP7qBuFL7XHUwMDEyYDCwZStyXHUwMDFj2+1VTmZcdTAwMWFcckqiLMZcdTAwMTQpkZRspVx1MDAxMWAwZ+hcdTAwMWNjljPlJPOKdkRKMlx1MDAxNcWb1EmUwLGruDxcdTAwMTa/t5fz+5NSaSVcdTAwMTl23ZVcdTAwMTelXHUwMDE197Lh+F4zci5WntnxgVx1MDAxYsVeXHUwMDE4wFx1MDAxNE1/jsN+1EiPbCdJN37x/HnHic7dpOs7XHJcdTAwMTdccry47/hx0m96IWqEnede4nbiv9mvu07H/Ws37DSTXGJlN1l1m15cdTAwMTJGV/dyfbfjXHUwMDA2SVxmV/87/FxcKv2efs1JXHUwMDE3uY3EXHTOfDc9IZ3KXHUwMDA0JJjxyeHdMEilpVJcdCOVXHUwMDE5zXvxXHUwMDA23C5xmzDZXHUwMDAykd1sxlx1MDAwZa3UX56u8lajfr6zj/VvXHUwMDAz1yHRRT+7a8vz/cNk6KdSNaIwjlfbTtJoZ0fESVx1MDAxNJ67Na+ZtD8vXm58dG5cdTAwMWPCQmRnRWH/rFx1MDAxZLixXVx1MDAwMzJcdTAwMWFccrtOw0uG6TPi0ejVQrwoZSOX9k6UIcxcZlWGXHUwMDFhZrRQajRtLyAl0lpcdTAwMWGuXHUwMDE55lRzbiZcdTAwMDQrhz68XHUwMDBlXHUwMDEw7ClOP5lodadxflx1MDAwNvJcdTAwMDXN0TFJ5Fx1MDAwNHHXieClZcddXFw/slx1MDAwMEGYMEJJw5RmLJOj7Xpn7Vx1MDAwNFx1MDAwZWFcdTAwMTIjTbDhXFwyJoimMpPGTV9cclx1MDAxMVxcK1xml8jOtjJ0t5opJb9mLyRcdTAwMDK+tuwpQd/38+tcdTAwMTk0r9dzbKJuJzZzwGWX6nebzlx1MDAxNVx1MDAxOERcdTAwMTEmKeZSgJSjed9cdTAwMGLOJy/nh43zjKV09MOzWzCsjChCmGCl4aVcbknmhtjpVVx1MDAwNqe7XHUwMDA3bqvSOKqel/1cdTAwMWHFXHUwMDFi61x1MDAwNVx1MDAxME+AuDh8OdJcdTAwMDLAoFx1MDAxNFOjXGah4/hqhGFcdTAwMWSoxFx1MDAwMpBRWj9cdTAwMWO+xCBDODVcXGqBQZBpfKlGQlx0hTU2RmEpmZ7GXHUwMDE3c0OtXHUwMDFhfjP4ur7vdeNcdTAwMWLhhftcdTAwMTTBK7li5Gvs78nxK+dQXGYvejtcdTAwMDde69xcdTAwMDSV+JeXRfZ3Jrrk8dBlXHUwMDFjXHSARVx1MDAwMppEXHUwMDEzg8ctr5JAXHUwMDBio5JcdTAwMDHZXHUwMDE4kOJ3Qfdpy1x1MDAxMVTQaWxcdENcdTAwMWN0XHUwMDA3nFx1MDAxZOhcdTAwMGblWvNpblx0XHUwMDA1OSU3YHIxZYxyNcktXHUwMDAwXHUwMDBilvtbsrozsFU5XHUwMDFiM4Gt4kRcdTAwMWH4Oze2g+5cdTAwMWXpuoPN9ZbgZ69cdTAwMTmpXVbLtSXHVlx1MDAxMsSNXHUwMDExlFBcclx1MDAxNlx1MDAxNYtxalx1MDAwNThxooQ2XHUwMDE4XFwx0fKO1NYxXHUwMDE2XHUwMDBmRS1cdTAwMDSANtigmHxcdTAwMTfYSlWELVx1MDAwM3NLhNBsbm533uDt/dPN3v4gpju7XXZcdTAwMTJcdTAwMGZcdTAwMWG/LTm32iCwgYJDLKshvuTj3EJcdTAwMTiBOVx1MDAwNJZcdTAwMWNsMeNE3IlbQutcdTAwMTA1P1x1MDAxOLdKQKzOXHQ131xmt4l7mdxcdTAwMThcIlx1MDAxNEZcYlxcW88oclB/idmzzsn62/CiXHUwMDE19Fx1MDAwZvc3TpOeXGLeXHUwMDBivdzRLVx1MDAxN1x1MDAxNCmIXHIwY0bAs04kZ1xuI8CDXHUwMDE5XGJcdTAwMWQ0VzznfiehJa79c/voXHUwMDE2zDpEKIaBJJgqxm/glopJTjlIXHUwMDA1XGbrXHUwMDA1Yio1hORUf1x1MDAwNaaZVGGQXHUwMDFjeu/Talx1MDAwMFx1MDAxZVx1MDAxYq04XHUwMDFkz1x1MDAxZo691JTflOOo41x1MDAwNY6/Mjaz5ntnluZcdTAwMTXfbY1jnnhccsdcdTAwMWZNJ2E3m23AnVx1MDAxYy9wo+mVXHQj78ze5ajwrvCcbnVkUVDu3dSd2LWzdlxc30oniTCTo6OckzCqXHUwMDE1pGPzR0CXeu2UvD5cdTAwMWGcvCtv1JPGsLKvPLLcWkmZLYpcdTAwMTBuuMBgwlxyXHUwMDFlzzmpxogxQVx1MDAxNbf+RIviksldtZJcdTAwMTKMXGZT2cvNlDGXLVxcKSPF0qQ2YoGp5ZU2mjyl96iNWfTyWVx1MDAxYv9S6lx1MDAwZZN2XHUwMDE4lLzAXCKPusPH1ctZ95/U0Fx1MDAxYlx1MDAxNdTcUkHJ5OhIQVx1MDAwNbYhXHUwMDA1o/O7TYdcZumx13ldebllZCu52KItvrPkXG5KJVx1MDAwMlx1MDAxZsVcdTAwMDSXRFKFc4FtXG5cbjZIQZpcdTAwMDYpNzdE6clcdTAwMTg0U1DaMi7ndylcblx0JOlccvpJclx1MDAxMF8rKCNcdTAwMTCFa/pcdTAwMWSp56eP//70xz9cdTAwMTf59+N//lx1MDAxMXz6418lr9NccqOklLS9uDTzXHUwMDAzXHUwMDA3X53RXG6jktXrXHUwMDEyvP4z9yeCf37xhTPsp1x1MDAxYnlB8lPw81x1MDAxY/f4+L9Fr81/XHUwMDFm11j+oOHPQsNcXK6LiJm+a3ZnjnA5OTzyYJA9U1tcdTAwMWKe24GpzfiClVtcdTAwMGX1anHjpFLeXHUwMDFjen7052jNXHTEId9cdTAwMTKUgF9cdTAwMThrV1x1MDAwZdP0ykBgr4WEIFx1MDAxM1x1MDAxNlx1MDAwNcs71SxcdTAwMWWhN6chMIHogyy8uUHyKD9Yby5f0J/qzVx0SNgxJ/NT3H7/+vByUD1+e+iL9XpMnMBcdTAwMWK2lz1cZlOIci6UwMZcdTAwMDae48VcdTAwMGJu0yRcdTAwMDW8QDjKJeeYPVx1MDAxY7730puDlFx1MDAwZkIkyG+/XHUwMDE5fGdVi1x1MDAxNS6C1+a98FLN/H3lkzZda1x1MDAxZZQjUj1uXHUwMDFlXHUwMDFjNdpq66R6seTVYsjxXHJYPKZcdTAwMDVcdTAwMDXDy+SE7dVcdTAwMWFcdTAwMDG6WsExmuW7zsvXnKPEdpRccv4+wNW0cFOPzfkgIzTztzmq+28q4dbh+erQK7d5r856tfLakoMrXHUwMDE50jaxXHUwMDA150JcdTAwMTlcdTAwMTZ40uoqXHUwMDA040yJtHt3p/1cdTAwMTBcdTAwMGbcnlx1MDAwM72TWkqyyIrVI3Kriquq1LZ7zNdY3Jc7nlx1MDAxYuNcdTAwMWVf9VuDau3dpnpVq95qJ88jgmtcYjhpKYhSVLHc1pDP2Fx1MDAxMiWxpFx1MDAxMGOSO8Zcblx1MDAwZtye00pcdI5cdTAwMDVZeFf53rAtLDTqwiCBMNugs3ZobmYvutH+5j7ZXHUwMDEwu1x1MDAxYvuy2Ze7ZH/TXe5cYpdcdTAwMGKBIOfBXG645Fx1MDAxOPLSySiBXCJbZSRcdTAwMTAvSVx1MDAwNS+keFx1MDAwZs9CXHUwMDFhdFx1MDAwNljHkK0tvEH3UDXH77NBR1Vx/Z/BmlOt8fyJ53FP7jjbtdO1zdW9gyZdq78/7peXWy0h3kVUUS5cdTAwMTUziimWlVwi0j2hhlwiSPFsRFxm5Fx1MDAxOVpcXP5/zP6cLVx0gFx1MDAwMqtcdTAwMWb9ueyYb7Q/XHUwMDA3IXqhfnJqgcFfUVx1MDAxOPLXvM6GqO329ereK1V/XelttypLrp9UI1xi8Vx1MDAxOIUn5ZBCT2zFolx1MDAxNFx1MDAxMckoXHUwMDEznCqueXGs96jtOaGxXmhQl2on+5otV3fTzlx1MDAxZlxymVx1MDAxOWcsVUPmR3vuXHUwMDA3XHKP2Z6jhWmfpmCoXHUwMDE4IfOX2EhvfbuqlcvLb99eXjr97kbz6HLJ3Vx1MDAxN+OIQV5cdTAwMDc+Styw/1x1MDAwYuJNJLFcdTAwMDT/JpWcVVx1MDAxYXYxI4zMdF9PW62GaZhcdTAwMWJKXHUwMDE1XHUwMDE0XHQsXHUwMDA1xpRzwvGNXowypLUxIIfdlkem8z/wK5xA1irvIVx1MDAwMfyMUFx1MDAwNlx1MDAxMbtcdTAwMWX5UOzsRudkZ4/AONi+PN9cdTAwMWVe+q3tpNojvai25Veyus5cdTAwMTitTlx1MDAxNIVcdTAwMTcro5lcdTAwMGbX381ypZLl+6VcdTAwMGbZXHUwMDA2pKSwIM1cdTAwMTTmkI3kXHUwMDA0+ZK2bPT97aNcck81XHUwMDBl+/XtNVapbe3t9e5VW5phYm9/j+pcdTAwMDKJXHUwMDE4YkZAslx1MDAwNeGTytec7flcdTAwMWFzZCRcdTAwMTd2+5PUbEa0t3h1XHUwMDExttZjf1liQeryaFx1MDAwNb8rhbpcdTAwMDFmZoo3Zlx1MDAxMG53vOr5XHUwMDBifrM1fFx1MDAwMaZffVx1MDAxMWWGXHUwMDExNlxubD9k6oTh8X2Fmk7sx79LYaFcdTAwMTBlrjRcdTAwMDKGpaKQjlxiIFlOo6xcdTAwMDRcYmJ/aVBcdTAwMThObJljXG5larRS1NxH6W9cdTAwMWFlelx1MDAxN5RT26xulebEiVx1MDAxMyXrXtD0grPJU9ygmc3kRL7+T1x1MDAwNLbmXGJG0pSp0bfyr2LEIVFnRHNJMDNgMXjusDOna99cdTAwMTSCjI1rMFx1MDAxOFQoa1CmlsV34qRcdTAwMWN2Op61ub+EXHUwMDEwb06KnT7SmlXHtutMvVx1MDAwNnio/Nyk3nbtXHUwMDE1x1x1MDAxZG32XSljO/1h9P2vz248epVyg6RUXHUwMDAwN4RccpCW6/zpq1xuKWNgkCtpofzi1YoxvrrcJMHZXHUwMDA1n+T/vZ1LxoVcdTAwMDVcdTAwMTj7aNpQOn9cdTAwMDB7vMferVV3XHUwMDFijvP2/M3uprOxeeRcdTAwMWQtd1x1MDAwMKvA1UlcYl+lxvC4dLw8XG4mXHUwMDFlaSo45pxSlf8ls6VzyFx1MDAwNixcdTAwMTiwI+6hZHrP/lhqXHUwMDEwLVx1MDAxNyfM64+fXFwrzIrT7Vx1MDAxZSZwyZFo8Ghe87qGk11mZeC5XHUwMDE36zeuu/3YmDiV32Lops/54cmH/1x1MDAwM7lI91xmIn0= terminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256fterminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256f

    There is an additional consideration that comes in to play when the output has less lines than the previous frame. If we were to write a shorter frame, it wouldn't fully overwrite the previous frame. We would be left with a few lines of a previous frame that wouldn't update.

    The solution to this problem is to write an escape code that clears lines from the cursor downwards before we write a smaller frame. You can see this in action in the above video. The inline app can grow or shrink in size, and still be anchored to the bottom of the terminal.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#cursor-input","title":"Cursor input","text":"

    The cursor tells the terminal where any text will be written by the app, but it also assumes this will be where the user enters text. If you enter CJK (Chinese Japanese Korean) text in to the terminal, you will typically see a floating control that points where new text will be written. If you are on a Mac, the emoji entry dialog (Ctrl+Cmd+Space) will also point at the current cursor position. To make this work in a sane way, we need to move the terminal's cursor to where any new text will appear.

    The following diagram shows the cursor moving to the point where new text is displayed.

    eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2a627bRlx1MDAxNoD/5ylcdTAwMDRlf7RAzMzMmWuAxcJ27Nhx41xc3NSut0VAkyOJNUWyJGVbKVxmXHUwMDE0fYbmMbq7z5Qn2TO0I0qUZKuOb73Qhi3O9fDMd+acM9RPXHUwMDBmWq12Ocxs+0mrbU9cdTAwMDI/jsLcP24/cuVHNi+iNMEqVt1cdTAwMTfpIFx1MDAwZqqWvbLMiiePXHUwMDFm9/380JZZ7Fx1MDAwN9Y7ioqBXHUwMDFmXHUwMDE35SCMUi9I+4+j0vaLf7m/237f/jNL+2GZe/UkSzaMyjQ/m8vGtm+TssDR/433rdZP1d8x6XJcdTAwMWKUftKNbdWhqqpcdTAwMDWkjJlm8XaaVNJSobk2Wkkxalx1MDAxMVx1MDAxNU9xwtKGWN1BoW1d44rar9+Q7GiLraytL29mS/L5yTc/RLv1vJ0ojnfKYVxcyVx1MDAxNeRpUSz1/DLo1S2KMk9cdTAwMGbtblx1MDAxNJa9T+pcdTAwMWIrXHUwMDFm9S1SVEXdK09cdTAwMDfdXmJcdTAwMGKnXHUwMDA1OipNMz+IyqErI2RUeqaKJ6265Fx1MDAwNO+48oBcdTAwMTFGqVx1MDAwMEo0UC5H1WdcdTAwMDNIT3AlJZdMXHRcdTAwMDGsKdlqXHUwMDFh44qgZFx1MDAwZkl11bJcdTAwMWT4wWFcdTAwMTdcdTAwMDVMwlGbMveTXCLzc1xct7rd8fkzXHUwMDBiXHUwMDA2XHUwMDFlXGIjlDSgNIBcdTAwMWG16Nmo2yuxXHRI4mlKXGbnXHUwMDEyQFDNamFcdTAwMGJbrY0mRoBSXHUwMDEyRlx1MDAxNU6EbDOsOPm+XpBcdTAwMWNcdNt0PZJBXHUwMDFjj+szXHTP9TlRceAq1saQq4dcdTAwMWFkoX9cdTAwMDZcdTAwMDZVXHUwMDE0JJOaaSlquOIoOWxcdTAwMGVcdTAwMTenwWHNUlV6+uhcblx1MDAxNFx1MDAxYkPmQqy0MIpcdTAwMThtXHUwMDE2hvjHr7rrq2+Wj9f2uqTc3szWNlx1MDAwM39jXHUwMDBlxFxyXHUwMDEw71xmX+NR4Fx1MDAwMjjnhCjJTYNe5UmpXHUwMDA1Z6CMJOPV104vNZ6hnFx1MDAxOY7zXHUwMDExZeg0vUx7Qlx0RZBRXFxcdTAwMTgpQTfpXHUwMDE1XHUwMDFjgKEhij9ccr02jqOsmMmuXHUwMDE2MI9dzXEtJfDF0e2+j168XHUwMDEw2TqYcqhXzP7zb/rv315cdTAwMDVdemvoXG7taW64UVpqQyVcdTAwMTI8yS6VXHUwMDFlXHUwMDAzMMo4KnBTa0r2u9h92PFcdTAwMDVcdTAwMTNsmltcblx1MDAxZSeMXHQj0Vx1MDAwYjCuNZ9cdTAwMDaXMk+gXHUwMDA3MLjlXHUwMDEyXHUwMDE0iXHVXHUwMDA01/UlnCui/lxu5Jqxx2ySXHUwMDBiwJkyv4Pc7bWXXHUwMDFi5bF+2SGr79ZcdTAwMGZiXHUwMDE28/f7nftNrlx1MDAxNp7SXHUwMDA0d13BiFZKq1x1MDAwNrjCI1x1MDAxNCFcdTAwMTGMaSXImJlfXHLcXHUwMDAzQsRNgUu5llxcMKn+XHUwMDEyWy7qalx1MDAxZbhC4V4jcVFcdTAwMTdcdTAwMDZ36+nhXHUwMDBifTLs7m1nvb2NV1svI1x1MDAwMUf3XHUwMDFiXFyK0GhcZlx1MDAwM1x1MDAxNG5Uwlx1MDAwMLrpXHUwMDA2udzjUtAq1EVi9GdcdTAwMDW7XHUwMDBmKTvQWt5cdTAwMTi5Qlx1MDAxYuCamj9cdTAwMGa5pT0pZ2HL5NxIgUpCccVAwcLc/rD79sSHr0Gvvn6z+3SfLNFnavN+R7lMSc9grFx1MDAwZkJcdTAwMDKT+LyNUIFg7Ek0brycUMVByrncUut+rlx1MDAxZeYqgVx0mDGYK1x1MDAwMmFcbvhcZnSZmEJVXHUwMDAyZcDUWFxi81x1MDAwN0C1lipNyp3ova3inInSdb9cdTAwMWbFw4llrVx1MDAxOK5YzvtR4sftiZrlOOo6otux7UyiXkaBXHUwMDFmj6rLNKtrXHUwMDAznMmPXHUwMDEym09rJs2jrpvl67mz4nPajdGu4o0tzoFfWFfryvWV7Fx1MDAxMoRolo7sXHUwMDEyWUVcdTAwMWUx3VnYLjl59tan20d+9JzRaOdN9mpJsfttl5J66Cw4hlx1MDAwZlxcYOJcdTAwMDeT7lx1MDAwNFxcSmhcdTAwMDTarZIuir85q2SUeFx1MDAwNsZimNpcdTAwMThrXHUwMDAz/WSMXG4loZzDXHUwMDFk+1xyReg4pNdojLUr+GSM/2hlw7KXJq0occR72fB2zfKi+ZtcdTAwMDY60z7N1eyTz81ThDuA0Fx1MDAxNOo1uMw8i63lYfx8XHUwMDA11nskXGJ3XHUwMDBls414S2T32zxcdTAwMDXxJCa9mF9cdTAwMGJcdTAwMDBkrlx1MDAxZaXChFx1MDAxYlx1MDAwZkP/KrdmnEvdkKs2T9YxlvPPOVx1MDAxYlx1MDAxMk6QWVFeLfDIPCWG4VxmQ9C7Nk82juiNmufHXHUwMDBmv3389ee7/P3wn++Sj7/+0or6WZqXrbJcdTAwMTdcdTAwMTWtXHUwMDBiL2x81qOT5i1n1y1cdTAwMDSga7+g5Msnl/RwV5ZHSflF8uVcdTAwMDJzfPjfXevmv7e7Wf5Nw1x1MDAxZoWGhVxcXHUwMDE3XHUwMDE1XHUwMDE3+q5cdTAwMGJfb1x1MDAwMJ3rwDDrk1x1MDAxMjd1tXiA2Vx1MDAwYsPO9ru9XHUwMDFmXHUwMDA3sYSlwVx0W9/v7r+731x1MDAxZYxhXGJcdIxi2qdcdTAwMThRXHUwMDA006hcdFx1MDAxN8ZcdTAwMTnxmCGcXGKXi4HiXHLBalx1MDAxN2axL9BLTto6gVx0zIzzXG7mXHRcIlx1MDAwNSHoJSknMz1cdTAwMTlcdTAwMDNPa1xmdkFSYJrOyFx1MDAwMYlcdTAwMDDsj20+3699gqjGXGLOS07nu7tRn7r3iIxju9594dM1sqo3l5dcdTAwMDa7+1n323ykiFx0Xv08T4/bo5rT80/343Ugp3PfaVx1MDAwM/IhheKLXHUwMDA3fGHk2+Dbbnm4WiztvVx1MDAxYa7oZ6+D19dqLmFauumvM+IznsRcXEtzQrTShky+zMbVQGvSXFxcdTAwMTJpwL1qvsfmXCJcdTAwMTU+giHmXHUwMDFhjkyuZC63XHUwMDA29JlBzYDZSD5cdTAwMGZmyo3WinJYfPO/2MTvYPNXl1x1MDAxZS5cYkxP8Fx1MDAxMSWbtfWDwuyFUFx1MDAwNVx1MDAwMIg7tvqcw4X5LFx1MDAwYuqhsinRWktFZ1x1MDAxZVVjloXJXHUwMDAyo0BcYroppae+k0E1+ih5Uyyz22V5TM1+Xq5ESVx1MDAxOCXdZlx1MDAxN5uEdc2YyOffWdpcXCBcdTAwMWOpsqZg4OQnuFx1MDAwNFxmd1x01LJcdTAwMTRcdTAwMTRcdTAwMTVdXHUwMDFmqjlV+pnTgoerQ1D51X5CzbSFx35Rrqb9fuT23FcpRpxNqasnWnbm2LP+1CrgM43XNe02cyNOOtr6U6tGu7pcdTAwMTl9/v7RzNZKeSCldF9cdTAwMDKQXHUwMDA0o5/xzlxcee49s+S0qmLissHmM+yuKXrr4Vx1MDAxZYz/d46+mqDtZ9lOiVx1MDAxNI2WXHUwMDE2aY7C86S3Vln7KLLHKzNtzV0uhKhcdTAwMTbHbT22Qvv0wen/XHUwMDAxc82RiyJ9 terminal$ python inline.py\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\u256e\u2502 import this \u2502\u2502 for n in range(10): \u2502\u2502 print(n) \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\u256f

    This only really impacts text entry (such as the Input and TextArea widgets).

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#mouse-control","title":"Mouse control","text":"

    Inline apps in Textual support mouse input, which works the same as fullscreen apps.

    To use the mouse in the terminal you send an escape code which tells the terminal to write encoded mouse coordinates to standard input. The mouse coordinates can then be parsed in much the same was as reading keys.

    In inline mode this works in a similar way, with an added complication that the mouse origin is at the top left of the terminal. In other words if you move the mouse to the top left of the terminal you get coordinate (0, 0), but the app expects (0, 0) to be where it was displayed.

    In order for the app to know where the mouse is relative to it's origin, we need to ask the terminal where the cursor is. We do this with an escape code, which tells the terminal to write the current cursor coordinate to standard input. We can then subtract that coordinate from the physical mouse coordinates, so we can send the app mouse events relative to its on-screen origin.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#tldr","title":"tl;dr","text":"

    Escapes codes.

    "},{"location":"blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/#found-this-interesting","title":"Found this interesting?","text":"

    If you are interested in Textual, join our Discord server.

    Or follow me for more terminal shenanigans.

    • @willmcgugan
    • mastodon.social/@willmcgugan
    "},{"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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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/2024/02/20/remote-memory-profiling-with-memray/","title":"Remote memory profiling with Memray","text":"

    Memray is a memory profiler for Python, built by some very smart devs at Bloomberg. It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!

    They recently added a Textual interface which looks amazing, and lets you monitor your process right from the terminal:

    You would typically run this locally, or over a ssh session, but it is also possible to serve the interface over the web with the help of textual-web. I'm not sure if even the Memray devs themselves are aware of this, but here's how.

    First install Textual web (ideally with pipx) alongside Memray:

    pipx install textual-web\n

    Now you can serve Memray with the following command (replace the text in quotes with your Memray options):

    textual-web -r \"memray run --live -m http.server\"\n

    This will return a URL you can use to access the Memray app from anywhere. Here's a quick video of that in action:

    "},{"location":"blog/2024/02/20/remote-memory-profiling-with-memray/#found-this-interesting","title":"Found this interesting?","text":"

    Join our Discord server if you want to discuss this post with the Textual devs or 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/2024/09/08/towards-textual-web-applications/","title":"Towards Textual Web Applications","text":"

    In this post we'll look at some new functionality available in Textual apps accessed via a browser and how it helps provide a more equal experience across platforms.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#what-is-textual-serve","title":"What is textual-serve?","text":"

    textual-serve is an open source project which allows you to serve and access your Textual app via a browser. The Textual app runs on a machine/server under your control, and communicates with the browser via a protocol which runs over websocket. End-users interacting with the app via their browser do not have access to the machine the application is running on via their browser, only the running Textual app.

    For example, you could install harlequin (a terminal-based SQL IDE) on a machine on your network, run it using textual-serve, and then share the URL with others. Anyone with the URL would then be able to use harlequin to query databases accessible from that server. Or, you could deploy posting (a terminal-based API client) on a server, and provide your colleagues with the URL, allowing them to quickly send HTTP requests from that server, right from within their browser.

    Accessing an instance of Posting via a web browser."},{"location":"blog/2024/09/08/towards-textual-web-applications/#providing-an-equal-experience","title":"Providing an equal experience","text":"

    While you're interacting with the Textual app using your web browser, it's not running in your browser. It's running on the machine you've installed it on, similar to typical server driven web app. This creates some interesting challenges for us if we want to provide an equal experience across browser and terminal.

    A Textual app running in the browser is inherently more accessible to non-technical users, and we don't want to limit access to important functionality for those users. We also don't want Textual app developers to have to repeatedly check \"is the the end-user using a browser or a terminal?\".

    To solve this, we've created APIs which allow developers to add web links to their apps and deliver files to end-users in a platform agnostic way. The goal of these APIs is to allow developers to write applications knowing that they'll provide a sensible user experience in both terminals and web browsers without any extra effort.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#opening-web-links","title":"Opening web links","text":"

    The ability to click on and open links is a pretty fundamental expectation when interacting with an app running in your browser.

    Python offers a webbrowser module which allows you to open a URL in a web browser. When a Textual app is running in a terminal, a simple call to this module does exactly what we'd expect.

    If the app is being used via a browser however, the webbrowser module would attempt to open the browser on the machine the app is being served from. This is clearly not very useful to the end-user!

    To solve this, we've added a new method to Textual: App.open_url. When running in the terminal, this will use webbrowser to open the URL as you would expect.

    When the Textual app is being served and used via the browser however, the running app will inform textual-serve, which will in turn tell the browser via websocket that the end-user is requesting to open a link, which will then be opened in their browser - just like a normal web link.

    The developer doesn't need to think about where their application might be running. By using open_url, Textual will ensure that end-users get the experience they expect.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#saving-files-to-disk","title":"Saving files to disk","text":"

    When running a Textual app in the terminal, getting a file into the hands of the end user is relatively simple - you could just write it to disk and notify them of the location, or perhaps open their $EDITOR with the content loaded into it. Given they're using a terminal, we can also make an assumption that the end-user is at least some technical knowledge.

    Run that same app in the browser however, and we have a problem. If you simply write the file to disk, the end-user would need to be able to access the machine the app is running on and navigate the file system in order to retrieve it. This may not be possible: they may not be permitted to access the machine, or they simply may not know how!

    The new App.deliver_text and App.deliver_binary methods are designed to let developers get files into the hands of end users, regardless of whether the app is being accessed via the browser or a terminal.

    When accessing a Textual app using a terminal, these methods will write a file to disk, and notify the App when the write is complete.

    In the browser, however, a download will be initiated and the file will be streamed via an ephemeral (one-time) download URL from the server that the Textual app is running on to the end-user's browser. If the app developer wishes, they can specify a custom file name, MIME type, and even whether the browser should attempt to open the file in a new tab or be downloaded.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#how-it-works","title":"How it works","text":"

    Input in Textual apps is handled, at the lowest level, by \"driver\" classes. We have different drivers for Linux and Windows, and also one for handling apps being served via web.

    When running in a terminal, the Windows/Linux drivers will read stdin, and parse incoming ANSI escape sequences sent by the terminal emulator as a result of mouse movement or keyboard interaction. The driver translates these escape sequences into Textual \"Events\", which are sent on to your application's message queue for asynchronous handling.

    For apps being served over the web, things are again a bit more complex. Interaction between the application and the end-user happens inside the browser - with a terminal rendered using xterm.js - the same front-end terminal engine used in VS Code. xterm.js fills the roll of a terminal emulator here, translating user interactions into ANSI escape codes on stdin.

    These escape codes are sent through websocket to textual-serve and then piped to the stdin stream of the Textual app which is running as a subprocess. Inside the Textual app, these can be processed and converted into events as normal by Textual's web driver.

    A Textual app also writes to the stdout stream, which is then read by your emulator and translated into visual output. When running on the web, this stdout is also sent over websocket to the end-user's browser, and xterm.js takes care of rendering.

    Although most of the data flowing back and forth from browser to Textual app is going to be ANSI escape sequences, we can in reality send anything we wish.

    To support file delivery we updated our protocol to allow applications to signal that a file is \"ready\" for delivery when one of the new \"deliver file\" APIs is called. An ephemeral, single-use, download link is then generated and sent to the browser via websocket. The front-end of textual-serve opens this URL and the file is streamed to the browser.

    This streaming process involves continuous delivery of encoded chunks of the file (using a variation of Bencode - the encoding used by BitTorrent) from the Textual app process to textual-serve, and then through to the end-user via the download URL.

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#the-result","title":"The result","text":"

    These new APIs close an important feature gap and give developers the option to build apps that can accessed via terminals or browsers without worrying that those on the web might miss out on important functionality!

    "},{"location":"blog/2024/09/08/towards-textual-web-applications/#found-this-interesting","title":"Found this interesting?","text":"

    Join our Discord server to chat to myself and other Textual developers.

    "},{"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":"blog/2024/02/11/file-magic-with-the-python-standard-library/","title":"File magic with the Python standard library","text":"

    I recently published Toolong, an app for viewing log files. There were some interesting technical challenges in building Toolong that I'd like to cover in this post.

    Python is awesome

    This isn't specifically Textual related. These techniques could be employed in any Python project.

    These techniques aren't difficult, and shouldn't be beyond anyone with an intermediate understanding of Python. They are the kind of \"if you know it you know it\" knowledge that you may not need often, but can make a massive difference when you do!

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#opening-large-files","title":"Opening large files","text":"

    If you were to open a very large text file (multiple gigabyte in size) in an editor, you will almost certainly find that it takes a while. You may also find that it doesn't load at all because you don't have enough memory, or it disables features like syntax highlighting.

    This is because most app will do something analogous to this:

    with open(\"access.log\", \"rb\") as log_file:\n    log_data = log_file.read()\n

    All the data is read in to memory, where it can be easily processed. This is fine for most files of a reasonable size, but when you get in to the gigabyte territory the read and any additional processing will start to use a significant amount of time and memory.

    Yet Toolong can open a file of any size in a second or so, with syntax highlighting. It can do this because it doesn't need to read the entire log file in to memory. Toolong opens a file and reads only the portion of it required to display whatever is on screen at that moment. When you scroll around the log file, Toolong reads the data off disk as required -- fast enough that you may never even notice it.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#scanning-lines","title":"Scanning lines","text":"

    There is an additional bit of work that Toolong has to do up front in order to show the file. If you open a large file you may see a progress bar and a message about \"scanning\".

    Toolong needs to know where every line starts and ends in a log file, so it can display a scrollbar bar and allow the user to navigate lines in the file. In other words it needs to know the offset of every new line (\\n) character within the file.

    This isn't a hard problem in itself. You might have imagined a loop that reads a chunk at a time and searches for new lines characters. And that would likely have worked just fine, but there is a bit of magic in the Python standard library that can speed that up.

    The mmap module is a real gem for this kind of thing. A memory mapped file is an OS-level construct that appears to load a file instantaneously. In Python you get an object which behaves like a bytearray, but loads data from disk when it is accessed. The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actual reading of the file to the OS.

    Here's the method that Toolong uses to scan for line breaks. Forgive the micro-optimizations, I was going for raw execution speed here.

        def scan_line_breaks(\n        self, batch_time: float = 0.25\n    ) -> Iterable[tuple[int, list[int]]]:\n        \"\"\"Scan the file for line breaks.\n\n        Args:\n            batch_time: Time to group the batches.\n\n        Returns:\n            An iterable of tuples, containing the scan position and a list of offsets of new lines.\n        \"\"\"\n        fileno = self.fileno\n        size = self.size\n        if not size:\n            return\n        log_mmap = mmap.mmap(fileno, size, prot=mmap.PROT_READ)\n        rfind = log_mmap.rfind\n        position = size\n        batch: list[int] = []\n        append = batch.append\n        get_length = batch.__len__\n        monotonic = time.monotonic\n        break_time = monotonic()\n\n        while (position := rfind(b\"\\n\", 0, position)) != -1:\n            append(position)\n            if get_length() % 1000 == 0 and monotonic() - break_time > batch_time:\n                break_time = monotonic()\n                yield (position, batch)\n                batch = []\n                append = batch.append\n        yield (0, batch)\n        log_mmap.close()\n

    This code runs in a thread (actually a worker), and will generate line breaks in batches. Without batching, it risks slowing down the UI with millions of rapid events.

    It's fast because most of the work is done in rfind, which runs at C speed, while the OS reads from the disk.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#watching-a-file-for-changes","title":"Watching a file for changes","text":"

    Toolong can tail files in realtime. When something appends to the file, it will be read and displayed virtually instantly. How is this done?

    You can easily poll a file for changes, by periodically querying the size or timestamp of a file until it changes. The downside of this is that you don't get notified immediately if a file changes between polls. You could poll at a very fast rate, but if you were to do that you would end up burning a lot of CPU for no good reason.

    There is a very good solution for this in the standard library. The selectors module is typically used for working with sockets (network data), but can also work with files (at least on macOS and Linux).

    Software developers are an unimaginative bunch when it comes to naming things

    Not to be confused with CSS selectors!

    The selectors module can tell you precisely when a file can be read. It can do this very efficiently, because it relies on the OS to tell us when a file can be read, and doesn't need to poll.

    You register a file with a Selector object, then call select() which returns as soon as there is new data available for reading.

    See watcher.py in Toolong, which runs a thread to monitors files for changes with a selector.

    Addendum

    So it turns out that watching regular files for changes with selectors only works with KqueueSelector which is the default on macOS. Disappointingly, the Python docs aren't clear on this. Toolong will use a polling approach where this selector is unavailable.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#textual-learnings","title":"Textual learnings","text":"

    This project was a chance for me to \"dogfood\" Textual. Other Textual devs have build some cool projects (Trogon and Frogmouth), but before Toolong I had only ever written example apps for docs.

    I paid particular attention to Textual error messages when working on Toolong, and improved many of them in Textual. Much of what I improved were general programming errors, and not Textual errors per se. For instance, if you forget to call super() on a widget constructor, Textual used to give a fairly cryptic error. It's a fairly common gotcha, even for experience devs, but now Textual will detect that and tell you how to fix it.

    There's a lot of other improvements which I thought about when working on this app. Mostly quality of life features that will make implementing some features more intuitive. Keep an eye out for those in the next few weeks.

    "},{"location":"blog/2024/02/11/file-magic-with-the-python-standard-library/#found-this-interesting","title":"Found this interesting?","text":"

    If you would like to talk about this post or anything Textual related, join us on the 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/hatch/","title":"<hatch>","text":"

    The <hatch> CSS type represents a character used in the hatch rule.

    "},{"location":"css_types/hatch/#syntax","title":"Syntax","text":"Value Description cross A diagonal crossed line. horizontal A horizontal line. left A left leaning diagonal line. right A right leaning diagonal line. vertical A vertical line."},{"location":"css_types/hatch/#examples","title":"Examples","text":""},{"location":"css_types/hatch/#css","title":"CSS","text":"
    .some-class {\n    hatch: cross green;\n}\n
    "},{"location":"css_types/hatch/#python","title":"Python","text":"
    widget.styles.hatch = (\"cross\", \"red\")\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 click (top left).

    "},{"location":"events/app_blur/","title":"AppBlur","text":"

    Bases: Event

    Sent when the app loses focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusOut, or when running via textual-web.

    "},{"location":"events/app_blur/#see-also","title":"See also","text":"
    • AppFocus
    "},{"location":"events/app_focus/","title":"AppFocus","text":"

    Bases: Event

    Sent when the app has focus.

    • Bubbles
    • Verbose
    Note

    Only available when running within a terminal that supports FocusIn, or when running via textual-web.

    "},{"location":"events/app_focus/#see-also","title":"See also","text":"
    • AppBlur
    "},{"location":"events/blur/","title":"Blur","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":"

    Bases: MouseEvent

    Sent when a widget is clicked.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/click/#see-also","title":"See also","text":"
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/descendant_blur/","title":"DescendantBlur","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\n

    The widget that was blurred (alias of widget).

    "},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was blurred.

    "},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantFocus
    • Focus
    "},{"location":"events/descendant_focus/","title":"DescendantFocus","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\n

    The widget that was focused (alias of widget).

    "},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
    widget\n

    The widget that was focused.

    "},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantBlur
    • Focus
    "},{"location":"events/enter/","title":"Enter","text":"

    Bases: Event

    Sent when the mouse is moved over a widget.

    Note that this event bubbles, so a widget may receive this event when the mouse moves over a child widget. Check the node attribute for the widget directly under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"events/enter/#textual.events.Enter.node","title":"node instance-attribute","text":"
    node = node\n

    The node directly under the mouse.

    "},{"location":"events/enter/#see-also","title":"See also","text":"
    • Click
    • Leave
    • MouseDown
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/focus/","title":"Focus","text":"

    Bases: Event

    Sent when a widget is focussed.

    • Bubbles
    • Verbose
    "},{"location":"events/focus/#see-also","title":"See also","text":"
    • AppBlur
    • AppFocus
    • Blur
    • DescendantBlur
    • DescendantFocus
    "},{"location":"events/hide/","title":"Hide","text":"

    Bases: Event

    Sent when a widget has been hidden.

    • Bubbles
    • Verbose

    Sent when any of the following conditions apply:

    • The widget is removed from the DOM.
    • The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
    • The widget has its display attribute set to False.
    • The widget's display style is set to \"none\".
    "},{"location":"events/key/","title":"Key","text":"

    Bases: InputEvent

    Sent when the user hits a key on the keyboard.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    The key that was pressed.

    required str | None

    A printable character or None if it is not printable.

    required"},{"location":"events/key/#textual.events.Key(key)","title":"key","text":""},{"location":"events/key/#textual.events.Key(character)","title":"character","text":""},{"location":"events/key/#textual.events.Key.aliases","title":"aliases instance-attribute","text":"
    aliases = _get_key_aliases(key)\n

    The aliases for the key, including the key itself.

    "},{"location":"events/key/#textual.events.Key.character","title":"character instance-attribute","text":"
    character = (\n    key\n    if len(key) == 1\n    else None if character is None else character\n)\n

    A printable character or None if it is not printable.

    "},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
    is_printable\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.key","title":"key instance-attribute","text":"
    key = key\n

    The key that was pressed.

    "},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
    name\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\n

    The corresponding name for every alias in aliases list.

    "},{"location":"events/leave/","title":"Leave","text":"

    Bases: Event

    Sent when the mouse is moved away from a widget, or if a widget is programmatically disabled while hovered.

    Note that this widget bubbles, so a widget may receive Leave events for any child widgets. Check the node parameter for the original widget that was previously under the mouse.

    • Bubbles
    • Verbose
    "},{"location":"events/leave/#textual.events.Leave.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was previously directly under the mouse.

    "},{"location":"events/leave/#see-also","title":"See also","text":"
    • Click
    • Enter
    • MouseDown
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/load/","title":"Load","text":"

    Bases: Event

    Sent when the App is running but before the terminal is in application mode.

    Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys.

    • Bubbles
    • Verbose
    "},{"location":"events/load/#see-also","title":"See also","text":"
    • Mount
    "},{"location":"events/mount/","title":"Mount","text":"

    Bases: Event

    Sent when a widget is mounted and may receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/mount/#see-also","title":"See also","text":"
    • Load
    • Unmount
    "},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

    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:

    Name Type Description Default Offset

    The position of the mouse when captured.

    required"},{"location":"events/mouse_capture/#textual.events.MouseCapture(mouse_position)","title":"mouse_position","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when captured.

    "},{"location":"events/mouse_capture/#see-also","title":"See also","text":"
    • capture_mouse
    • release_mouse
    • MouseRelease
    "},{"location":"events/mouse_down/","title":"MouseDown","text":"

    Bases: MouseEvent

    Sent when a mouse button is pressed.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_down/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_move/","title":"MouseMove","text":"

    Bases: MouseEvent

    Sent when the mouse cursor moves.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_move/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseScrollDown
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_release/","title":"MouseRelease","text":"

    Bases: Event

    Mouse has been released.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Offset

    The position of the mouse when released.

    required"},{"location":"events/mouse_release/#textual.events.MouseRelease(mouse_position)","title":"mouse_position","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease.mouse_position","title":"mouse_position instance-attribute","text":"
    mouse_position = mouse_position\n

    The position of the mouse when released.

    "},{"location":"events/mouse_release/#see-also","title":"See also","text":"
    • capture_mouse
    • release_mouse
    • MouseCapture
    "},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled down.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_scroll_down/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollUp
    • MouseUp
    "},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

    Bases: MouseEvent

    Sent when the mouse wheel is scrolled up.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_scroll_up/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseUp
    "},{"location":"events/mouse_up/","title":"MouseUp","text":"

    Bases: MouseEvent

    Sent when a mouse button is released.

    • Bubbles
    • Verbose

    See MouseEvent for the full list of properties and methods.

    "},{"location":"events/mouse_up/#see-also","title":"See also","text":"
    • Click
    • Enter
    • Leave
    • MouseDown
    • MouseEvent
    • MouseMove
    • MouseScrollDown
    • MouseScrollUp
    "},{"location":"events/paste/","title":"Paste","text":"

    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:

    Name Type Description Default str

    The text that has been pasted.

    required"},{"location":"events/paste/#textual.events.Paste(text)","title":"text","text":""},{"location":"events/paste/#textual.events.Paste.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was pasted.

    "},{"location":"events/print/","title":"Print","text":"

    Bases: Event

    Sent to a widget that is capturing print.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default str

    Text that was printed.

    required bool

    True if the print was to stderr, or False for stdout.

    False Note

    Python's print output can be captured with App.begin_capture_print.

    "},{"location":"events/print/#textual.events.Print(text)","title":"text","text":""},{"location":"events/print/#textual.events.Print(stderr)","title":"stderr","text":""},{"location":"events/print/#textual.events.Print.stderr","title":"stderr instance-attribute","text":"
    stderr = stderr\n

    True if the print was to stderr, or False for stdout.

    "},{"location":"events/print/#textual.events.Print.text","title":"text instance-attribute","text":"
    text = text\n

    The text that was printed.

    "},{"location":"events/resize/","title":"Resize","text":"

    Bases: Event

    Sent when the app or widget has been resized.

    • Bubbles
    • Verbose

    Parameters:

    Name Type Description Default Size

    The new size of the Widget.

    required Size

    The virtual size (scrollable size) of the Widget.

    required Size | None

    The size of the Widget's container widget.

    None"},{"location":"events/resize/#textual.events.Resize(size)","title":"size","text":""},{"location":"events/resize/#textual.events.Resize(virtual_size)","title":"virtual_size","text":""},{"location":"events/resize/#textual.events.Resize(container_size)","title":"container_size","text":""},{"location":"events/resize/#textual.events.Resize.container_size","title":"container_size instance-attribute","text":"
    container_size = (\n    size if container_size is None else container_size\n)\n

    The size of the Widget's container widget.

    "},{"location":"events/resize/#textual.events.Resize.size","title":"size instance-attribute","text":"
    size = size\n

    The new size of the Widget.

    "},{"location":"events/resize/#textual.events.Resize.virtual_size","title":"virtual_size instance-attribute","text":"
    virtual_size = virtual_size\n

    The virtual size (scrollable size) of the Widget.

    "},{"location":"events/screen_resume/","title":"ScreenResume","text":"

    Bases: Event

    Sent to screen that has been made active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_resume/#see-also","title":"See also","text":"
    • ScreenSuspend
    "},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

    Bases: Event

    Sent to screen when it is no longer active.

    • Bubbles
    • Verbose
    "},{"location":"events/screen_suspend/#see-also","title":"See also","text":"
    • ScreenResume
    "},{"location":"events/show/","title":"Show","text":"

    Bases: Event

    Sent when a widget is first displayed.

    • Bubbles
    • Verbose
    "},{"location":"events/unmount/","title":"Unmount","text":"

    Bases: Event

    Sent when a widget is unmounted and may no longer receive messages.

    • Bubbles
    • Verbose
    "},{"location":"events/unmount/#see-also","title":"See also","text":"
    • Mount
    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    "},{"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. Consider the following widget class:

    from textual.widgets import Static\n\nclass Alert(Static):\n    pass\n

    Alert widgets may be styled with the following CSS (to give them a red border):

    Alert {\n  border: solid red;\n}\n

    The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Alert (Python) class extends Static.

    Static {\n  background: blue;\n  border: rounded green;\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 Alert. When this happens, Textual will use the most recently defined sub-class. So Alert wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, Alert widgets would have a \"solid red\" border and not a \"rounded green\" border.

    "},{"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:

    • :blur Matches widgets which do not have input focus.
    • :dark Matches widgets in dark mode (where App.dark == True).
    • :disabled Matches widgets which are in a disabled state.
    • :enabled Matches widgets which are in an enabled state.
    • :focus-within Matches widgets with a focused child widget.
    • :focus Matches widgets which have input focus.
    • :inline Matches widgets when the app is running in inline mode.
    • :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=app.set_background('red')]Red[/]\n[@click=app.set_background('green')]Green[/]\n[@click=app.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=app.set_background('red')]Red[/]\n[@click=app.set_background('green')]Green[/]\n[@click=app.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=app.set_background('cyan')]Cyan[/]\n[@click=app.set_background('magenta')]Magenta[/]\n[@click=app.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.
    • focused invokes actions on the currently focused widget (if there is one).

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

    "},{"location":"guide/actions/#dynamic-actions","title":"Dynamic actions","text":"

    Added in version 0.61.0

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

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

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

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

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

    actions06.pyactions06.tcssOutput actions06.py
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Placeholder\n\nPAGES_COUNT = 5\n\n\nclass PagesApp(App):\n    BINDINGS = [\n        (\"n\", \"next\", \"Next\"),\n        (\"p\", \"previous\", \"Previous\"),\n    ]\n\n    CSS_PATH = \"actions06.tcss\"\n\n    page_no = reactive(0)\n\n    def compose(self) -> ComposeResult:\n        with HorizontalScroll(id=\"page-container\"):\n            for page_no in range(PAGES_COUNT):\n                yield Placeholder(f\"Page {page_no}\", id=f\"page-{page_no}\")\n        yield Footer()\n\n    def action_next(self) -> None:\n        self.page_no += 1\n        self.refresh_bindings()  # (1)!\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def action_previous(self) -> None:\n        self.page_no -= 1\n        self.refresh_bindings()  # (2)!\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def check_action(\n        self, action: str, parameters: tuple[object, ...]\n    ) -> bool | None:  # (3)!\n        \"\"\"Check if an action may run.\"\"\"\n        if action == \"next\" and self.page_no == PAGES_COUNT - 1:\n            return False\n        if action == \"previous\" and self.page_no == 0:\n            return False\n        return True\n\n\nif __name__ == \"__main__\":\n    app = PagesApp()\n    app.run()\n
    1. Prompts the footer to refresh, if bindings change.
    2. Prompts the footer to refresh, if bindings change.
    3. Guards the actions from running and also what keys are displayed in the footer.
    actions06.tcss
    #page-container {\n    # This hides the scrollbar\n    scrollbar-size: 0 0;\n}\n

    PagesApp Page\u00a00 \u00a0n\u00a0Next\u00a0\u258f^p\u00a0palette

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

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

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

    actions07.pyactions06.tcssOutput actions06.py
    from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Placeholder\n\nPAGES_COUNT = 5\n\n\nclass PagesApp(App):\n    BINDINGS = [\n        (\"n\", \"next\", \"Next\"),\n        (\"p\", \"previous\", \"Previous\"),\n    ]\n\n    CSS_PATH = \"actions06.tcss\"\n\n    page_no = reactive(0, bindings=True)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        with HorizontalScroll(id=\"page-container\"):\n            for page_no in range(PAGES_COUNT):\n                yield Placeholder(f\"Page {page_no}\", id=f\"page-{page_no}\")\n        yield Footer()\n\n    def action_next(self) -> None:\n        self.page_no += 1\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def action_previous(self) -> None:\n        self.page_no -= 1\n        self.query_one(f\"#page-{self.page_no}\").scroll_visible()\n\n    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:\n        \"\"\"Check if an action may run.\"\"\"\n        if action == \"next\" and self.page_no == PAGES_COUNT - 1:\n            return None  # (2)!\n        if action == \"previous\" and self.page_no == 0:\n            return None  # (3)!\n        return True\n\n\nif __name__ == \"__main__\":\n    app = PagesApp()\n    app.run()\n
    1. The bindings=True causes the footer to refresh when page_no changes.
    2. Returning None disables the key in the footer rather than hides it
    3. Returning None disables the key in the footer rather than hides it.
    actions06.tcss
    #page-container {\n    # This hides the scrollbar\n    scrollbar-size: 0 0;\n}\n

    PagesApp Page\u00a00 \u00a0n\u00a0Next\u00a0\u00a0p\u00a0Previous\u00a0\u258f^p\u00a0palette

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

    "},{"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_focus_next
    • action_focus_previous
    • action_focus
    • action_pop_screen
    • action_push_screen
    • action_quit
    • action_remove_class
    • action_screenshot
    • action_simulate_key
    • action_suspend_process
    • action_switch_screen
    • action_toggle_class
    • action_toggle_dark
    "},{"location":"guide/animation/","title":"Animation","text":"

    This 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 how the widget will change as time progresses:

    After 0sAfter 1sAfter 1.5sAfter 2s

    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/#run-inline","title":"Run inline","text":"

    Added in version 0.55.0

    You can also run apps in inline mode, which will cause the app to appear beneath the prompt (and won't go in to application mode). Inline apps are useful for tools that integrate closely with the typical workflow of a terminal.

    To run an app in inline mode set the inline parameter to True when you call App.run(). See Style Inline Apps for how to apply additional styles to inline apps.

    Note

    Inline mode is not currently supported on Windows.

    "},{"location":"guide/app/#ansi-colors","title":"ANSI colors","text":"

    Added in version 0.80.0

    Terminals support 16 theme-able ANSI colors, which you can personalize from your terminal settings. By default, Textual will replace these colors with its own color choices (see the FAQ for details).

    You can disable this behavior by setting ansi_color=True in the App constructor.

    We recommend the default behavior for full-screen apps, but you may want to preserve ANSI colors in inline apps.

    "},{"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+P 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'

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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\udd0eSearch\u00a0for\u00a0commands\u2026 Bell Ring\u00a0the\u00a0bell Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen Show\u00a0keys\u00a0and\u00a0help\u00a0panel Show\u00a0help\u00a0for\u00a0the\u00a0focused\u00a0widget\u00a0and\u00a0a\u00a0summary\u00a0of\u00a0available\u00a0keys \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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 Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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 Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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/#system-commands","title":"System commands","text":"

    Textual apps have a number of system commands enabled by default. These are declared in the App.get_system_commands method. You can implement this method in your App class to add more commands.

    To declare a command, define a get_system_commands method on your App. Textual will call this method with the screen that was active when the user summoned the command palette.

    You can add a command by yielding a SystemCommand object which contains title and help text to be shown in the command palette, and callback which is a callable to run when the user selects the command. Additionally, there is a discover boolean which when True (the default) shows the command even if the search import is empty. When set to False, the command will show only when there is input.

    Here's how we would add a command to ring the terminal bell (a super useful piece of functionality):

    command01.pyOutput command01.py
    from typing import Iterable\n\nfrom textual.app import App, SystemCommand\nfrom textual.screen import Screen\n\n\nclass BellCommandApp(App):\n    \"\"\"An app with a 'bell' command.\"\"\"\n\n    def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:\n        yield from super().get_system_commands(screen)  # (1)!\n        yield SystemCommand(\"Bell\", \"Ring the bell\", self.bell)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = BellCommandApp()\n    app.run()\n
    1. Adds the default commands from the base class.
    2. Adds a new command.

    BellCommandApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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\udd0eSearch\u00a0for\u00a0commands\u2026 Bell Ring\u00a0the\u00a0bell Light\u00a0mode Switch\u00a0to\u00a0a\u00a0light\u00a0background Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Save\u00a0screenshot Save\u00a0an\u00a0SVG\u00a0'screenshot'\u00a0of\u00a0the\u00a0current\u00a0screen Show\u00a0keys\u00a0and\u00a0help\u00a0panel Show\u00a0help\u00a0for\u00a0the\u00a0focused\u00a0widget\u00a0and\u00a0a\u00a0summary\u00a0of\u00a0available\u00a0keys \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\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 is a straightforward way of adding commands to your app. For more advanced integrations you can implement your own command providers.

    "},{"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.

    command02.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 four methods you can override in a command provider: startup, search, discover 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/#discover-method","title":"discover method","text":"

    The discover method is responsible for providing results (or discovery hits) that should be shown to the user when the command palette input is empty; this is to aid in command discoverability.

    Note

    Because discover hits are shown the moment the command palette is opened, these should ideally be quick to generate; commands that might take time to generate are best left for search -- use discover to help the user easily find the most important commands.

    discover is similar to search but with these differences:

    • discover accepts no parameters (instead of the search value)
    • discover yields instances of DiscoveryHit (instead of instances of Hit)

    Instances of DiscoveryHit contain information about how the hit should be displayed, an optional help string, and a callback which will be run if the user selects that command.

    "},{"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/command_palette/#changing-command-palette-key","title":"Changing command palette key","text":"

    You can change the key that opens the command palette by setting the class variable COMMAND_PALETTE_BINDING on your app.

    Prior to version 0.77.0, Textual used the binding ctrl+backslash to launch the command palette. Here's how you would restore the older key binding:

    class NewPaletteBindingApp(App):\n    COMMAND_PALETTE_BINDING = \"ctrl+backslash\"\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/#serve","title":"Serve","text":"

    The devtools can also serve your application in a browser. Effectively turning your terminal app in to a web application!

    The serve sub-command is similar to run. Here's how you can serve an app launched from a Python file:

    textual serve my_app.py\n

    You can also serve a Textual app launched via a command. Here's an example:

    textual serve \"textual keys\"\n

    The syntax for launching an app in a module is slightly different from run. You need to specify the full command, including python. Here's how you would run the Textual demo:

    textual serve \"python -m textual\"\n

    Textual's builtin web-server is quite powerful. You can serve multiple instances of your application at once!

    Tip

    Textual serve is also useful when developing your app. If you make changes to your code, simply refresh the browser to update.

    There are some additional switches for serving Textual apps. Run the following for a list:

    textual serve --help\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.79.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, LOGGING and WORKER. 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,\u00a0ansi=None)\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,\u00a0ansi=None)\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,\u00a0ansi=None)\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,\u00a0ansi=None)\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 its Changed message as follows:

    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.pyon_decorator.tcssOutput 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_decorator.tcss
    Screen {\n    align: center middle;\n    layout: horizontal;\n}\n\nButton {\n    margin: 2 4;\n}\n

    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.pyon_decorator.tcssOutput 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\"
    on_decorator.tcss
    Screen {\n    align: center middle;\n    layout: horizontal;\n}\n\nButton {\n    margin: 2 4;\n}\n

    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, pane=\"#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 Key(key='l',\u00a0character='l',\u00a0name='l'\u258eKey(key='r',\u00a0character='r',\u00a0name='r'\u258a Key(key='o',\u00a0character='o',\u00a0name='o'\u258eKey(key='l',\u00a0character='l',\u00a0name='l'\u2583\u2583\u258a Key(\u2586\u2586\u258eKey(key='d',\u00a0character='d',\u00a0name='d'\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u258ename='exclamation_mark',\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\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

    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/#focusable-widgets","title":"Focusable widgets","text":"

    Each widget has a boolean can_focus attribute which determines if it is capable of receiving focus. Note that can_focus=True does not mean the widget will always be focusable. For example, a disabled widget cannot receive focus even if can_focus is True.

    "},{"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. By default, Textual will focus the first focusable widget when the app starts.

    "},{"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\u00a0Add\u00a0Red\u00a0\u00a0g\u00a0Add\u00a0Green\u00a0\u00a0b\u00a0Add\u00a0Blue\u00a0\u258f^p\u00a0palette

    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').

    When you press a key, Textual will first check for a matching binding in the BINDINGS list of the currently focused widget. If no match is found, it will search upwards through the DOM all the way up to the App looking for a match.

    "},{"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.

    Both Enter and Leave bubble, so a widget may receive these events from a child widget. You can check the initial widget these events were sent to by comparing the node attribute against self in the message handler.

    "},{"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\" behavior 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!

    Tip

    See the Textual Query Sandbox project for an interactive way of experimenting with DOM queries.

    "},{"location":"guide/queries/#query-one","title":"Query one","text":"

    The query_one method is used to retrieve a single widget that matches a selector or a type.

    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 line of code:

    send_button = self.query_one(\"#send\")\n

    This will retrieve a widget with an ID of send, if there is exactly one. If there are no matching widgets, Textual will raise a NoMatches exception.

    You can also add a second parameter for the expected type, which will ensure that you get the type you are expecting.

    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).

    You can also specify a widget type in place of a selector, which will return a widget of that type. For instance, the following would return a Button instance (assuming there is a single Button).

    my_button = self.query_one(Button)\n
    "},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

    Apps and widgets also 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:

    • add_class Adds a CSS class (or classes) to matched widgets.
    • blur Blurs (removes focus) from matching widgets.
    • focus Focuses the first matching widgets.
    • refresh Refreshes matched widgets.
    • remove_class Removes a CSS class (or classes) from matched widgets.
    • remove Removes matched widgets from the DOM.
    • set_class Sets a CSS class (or classes) on matched widgets.
    • set Sets common attributes on a widget.
    • 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.
    "},{"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 behavior 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 10 \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\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\u250110%

    "},{"location":"guide/reactivity/#recompose","title":"Recompose","text":"

    An alternative to a refresh is recompose. If you set recompose=True on a reactive, then Textual will remove all the child widgets and call compose() again, when the reactive attribute changes. The process of removing and mounting new widgets occurs in a single update, so it will appear as though the content has simply updated.

    The following example uses recompose:

    refresh03.pyrefresh03.tcssOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\n\n\nclass Name(Widget):\n    \"\"\"Generates a greeting.\"\"\"\n\n    who = reactive(\"name\", recompose=True)  # (1)!\n\n    def compose(self) -> ComposeResult:  # (2)!\n        yield Label(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. Setting recompose=True will cause all child widgets to be removed and compose called again to add new widgets.
    2. This compose() method will be called when who is 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 \u258aPaul\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,\u00a0Paul!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

    While the end-result is identical to refresh02.py, this code works quite differently. The main difference is that recomposing creates an entirely new set of child widgets rather than updating existing widgets. So when the who attribute changes, the Name widget will replace its Label with a new instance (containing updated content).

    Warning

    You should avoid storing a reference to child widgets when using recompose. Better to query for a child widget when you need them.

    It is important to note that any child widgets will have their state reset after a recompose. For simple content, that doesn't matter much. But widgets with an internal state (such as DataTable, Input, or TextArea) would not be particularly useful if recomposed.

    Recomposing is slightly less efficient than a simple refresh, and best avoided if you need to update rapidly or you have many child widgets. That said, it can often simplify your code. Let's look at a practical example. First a version without recompose:

    recompose01.pyOutput
    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Digits\n\n\nclass Clock(App):\n\n    CSS = \"\"\"\n    Screen {align: center middle}\n    Digits {width: auto}\n    \"\"\"\n\n    time: reactive[datetime] = reactive(datetime.now, init=False)\n\n    def compose(self) -> ComposeResult:\n        yield Digits(f\"{self.time:%X}\")\n\n    def watch_time(self) -> None:  # (1)!\n        self.query_one(Digits).update(f\"{self.time:%X}\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.set_interval(1, self.update_time)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = Clock()\n    app.run()\n
    1. Called when the time attribute changes.
    2. Update the time once a second.

    Clock \u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574 \u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u2570\u2500\u256e \u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2576\u2500\u256f

    This displays a clock which updates once a second. The code is straightforward, but note how we format the time in two places: compose() and watch_time(). We can simplify this by recomposing rather than refreshing:

    recompose02.pyOutput
    from datetime import datetime\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Digits\n\n\nclass Clock(App):\n\n    CSS = \"\"\"\n    Screen {align: center middle}\n    Digits {width: auto}\n    \"\"\"\n\n    time: reactive[datetime] = reactive(datetime.now, recompose=True)\n\n    def compose(self) -> ComposeResult:\n        yield Digits(f\"{self.time:%X}\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    app = Clock()\n    app.run()\n

    Clock \u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574 \u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u2570\u2500\u256e \u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2576\u2500\u256f

    In this version, the app is recomposed when the time attribute changes, which replaces the Digits widget with a new instance and updated time. There's no need for the watch_time method, because the new Digits instance will already show the current time.

    "},{"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/reactivity/#setting-reactives-without-superpowers","title":"Setting reactives without superpowers","text":"

    You may find yourself in a situation where you want to set a reactive value, but you don't want to invoke watchers or the other super powers. This is fairly common in constructors which run prior to mounting; any watcher which queries the DOM may break if the widget has not yet been mounted.

    To work around this issue, you can call set_reactive as an alternative to setting the attribute. The set_reactive method accepts the reactive attribute (as a class variable) and the new value.

    Let's look at an example. The following app is intended to cycle through various greeting when you press Space, however it contains a bug.

    set_reactive01.py
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive, var\nfrom textual.widgets import Label\n\nGREETINGS = [\n    \"Bonjour\",\n    \"Hola\",\n    \"\u3053\u3093\u306b\u3061\u306f\",\n    \"\u4f60\u597d\",\n    \"\uc548\ub155\ud558\uc138\uc694\",\n    \"Hello\",\n]\n\n\nclass Greeter(Horizontal):\n    \"\"\"Display a greeting and a name.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Greeter {\n        width: auto;\n        height: 1;\n        & Label {\n            margin: 0 1;\n        }\n    }\n    \"\"\"\n    greeting: reactive[str] = reactive(\"\")\n    who: reactive[str] = reactive(\"\")\n\n    def __init__(self, greeting: str = \"Hello\", who: str = \"World!\") -> None:\n        super().__init__()\n        self.greeting = greeting  # (1)!\n        self.who = who\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.greeting, id=\"greeting\")\n        yield Label(self.who, id=\"name\")\n\n    def watch_greeting(self, greeting: str) -> None:\n        self.query_one(\"#greeting\", Label).update(greeting)  # (2)!\n\n    def watch_who(self, who: str) -> None:\n        self.query_one(\"#who\", Label).update(who)\n\n\nclass NameApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n    greeting_no: var[int] = var(0)\n    BINDINGS = [(\"space\", \"greeting\")]\n\n    def compose(self) -> ComposeResult:\n        yield Greeter(who=\"Textual\")\n\n    def action_greeting(self) -> None:\n        self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)\n        self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]\n\n\nif __name__ == \"__main__\":\n    app = NameApp()\n    app.run()\n
    1. Setting this reactive attribute invokes a watcher.
    2. The watcher attempts to update a label before it is mounted.

    If you run this app, you will find Textual raises a NoMatches error in watch_greeting. This is because the constructor has assigned the reactive before the widget has fully mounted.

    The following app contains a fix for this issue:

    set_reactive02.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive, var\nfrom textual.widgets import Label\n\nGREETINGS = [\n    \"Bonjour\",\n    \"Hola\",\n    \"\u3053\u3093\u306b\u3061\u306f\",\n    \"\u4f60\u597d\",\n    \"\uc548\ub155\ud558\uc138\uc694\",\n    \"Hello\",\n]\n\n\nclass Greeter(Horizontal):\n    \"\"\"Display a greeting and a name.\"\"\"\n\n    DEFAULT_CSS = \"\"\"\n    Greeter {\n        width: auto;\n        height: 1;\n        & Label {\n            margin: 0 1;\n        }\n    }\n    \"\"\"\n    greeting: reactive[str] = reactive(\"\")\n    who: reactive[str] = reactive(\"\")\n\n    def __init__(self, greeting: str = \"Hello\", who: str = \"World!\") -> None:\n        super().__init__()\n        self.set_reactive(Greeter.greeting, greeting)  # (1)!\n        self.set_reactive(Greeter.who, who)\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.greeting, id=\"greeting\")\n        yield Label(self.who, id=\"name\")\n\n    def watch_greeting(self, greeting: str) -> None:\n        self.query_one(\"#greeting\", Label).update(greeting)\n\n    def watch_who(self, who: str) -> None:\n        self.query_one(\"#who\", Label).update(who)\n\n\nclass NameApp(App):\n\n    CSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\n    greeting_no: var[int] = var(0)\n    BINDINGS = [(\"space\", \"greeting\")]\n\n    def compose(self) -> ComposeResult:\n        yield Greeter(who=\"Textual\")\n\n    def action_greeting(self) -> None:\n        self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)\n        self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]\n\n\nif __name__ == \"__main__\":\n    app = NameApp()\n    app.run()\n
    1. The attribute is set via set_reactive, which avoids calling the watcher.

    NameApp HelloTextual

    The line self.set_reactive(Greeter.greeting, greeting) sets the greeting attribute but doesn't immediately invoke the watcher.

    "},{"location":"guide/reactivity/#mutable-reactives","title":"Mutable reactives","text":"

    Textual can detect when you set a reactive to a new value, but it can't detect when you mutate a value. In practice, this means that Textual can detect changes to basic types (int, float, str, etc.), but not if you update a collection, such as a list or dict.

    You can still use collections and other mutable objects in reactives, but you will need to call mutate_reactive after making changes for the reactive superpowers to work.

    Here's an example, that uses a reactive list:

    set_reactive03.pyOutput
    from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Label\n\n\nclass MultiGreet(App):\n    names: reactive[list[str]] = reactive(list, recompose=True)  # (1)!\n\n    def compose(self) -> ComposeResult:\n        yield Input(placeholder=\"Give me a name\")\n        for name in self.names:\n            yield Label(f\"Hello, {name}\")\n\n    def on_input_submitted(self, event: Input.Changed) -> None:\n        self.names.append(event.value)\n        self.mutate_reactive(MultiGreet.names)  # (2)!\n\n\nif __name__ == \"__main__\":\n    app = MultiGreet()\n    app.run()\n
    1. Creates a reactive list of strings.
    2. Explicitly mutate the reactive list.

    MultiGreet \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 \u258aGive\u00a0me\u00a0a\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 Hello,\u00a0Will

    Note the call to mutate_reactive. Without it, the display would not update when a new name is appended to the list.

    "},{"location":"guide/reactivity/#data-binding","title":"Data binding","text":"

    Reactive attributes may be bound (connected) to attributes on child widgets, so that changes to the parent are automatically reflected in the children. This can simplify working with compound widgets where the value of an attribute might be used in multiple places.

    To bind reactive attributes, call data_bind on a widget. This method accepts reactives (as class attributes) in positional arguments or keyword arguments.

    Let's look at an app that could benefit from data binding. In the following code we have a WorldClock widget which displays the time in any given timezone.

    Note

    This example uses the pytz library for working with timezones. You can install pytz with pip install pytz.

    world_clock01.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\")\n        yield WorldClock(\"Europe/Paris\")\n        yield WorldClock(\"Asia/Tokyo\")\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def watch_time(self, time: datetime) -> None:\n        for world_clock in self.query(WorldClock):  # (1)!\n            world_clock.time = time\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    app = WorldClockApp()\n    app.run()\n
    1. Update the time reactive attribute of every WorldClock.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u2576\u256e\u00a0\u256d\u2500\u2574\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u256e\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u2576\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u2576\u2500\u256e\u2576\u2500\u256e\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u250c\u2500\u2518\u250c\u2500\u2518\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2570\u2500\u2574\u2570\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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

    We've added three world clocks for London, Paris, and Tokyo. The clocks are kept up-to-date by watching the app's time reactive, and updating the clocks in a loop.

    While this approach works fine, it does require we take care to update every WorldClock we mount. Let's see how data binding can simplify this.

    The following app calls data_bind on the world clock widgets to connect the app's time with the widget's time attribute:

    world_clock02.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\").data_bind(WorldClockApp.time)  # (1)!\n        yield WorldClock(\"Europe/Paris\").data_bind(WorldClockApp.time)\n        yield WorldClock(\"Asia/Tokyo\").data_bind(WorldClockApp.time)\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    WorldClockApp().run()\n
    1. Bind the time attribute, so that changes to time will also change the time attribute on the WorldClock widgets. The data_bind method also returns the widget, so we can yield its return value.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u2576\u256e\u00a0\u256d\u2500\u2574\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u256e\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u2576\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u2576\u2500\u256e\u2576\u2500\u256e\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u250c\u2500\u2518\u250c\u2500\u2518\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2570\u2500\u2574\u2570\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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

    Note how the addition of the data_bind methods negates the need for the watcher in world_clock01.py.

    Note

    Data binding works in a single direction. Setting time on the app updates the clocks. But setting time on the clocks will not update time on the app.

    In the previous example app, the call to data_bind(WorldClockApp.time) worked because both reactive attributes were named time. If you want to bind a reactive attribute which has a different name, you can use keyword arguments.

    In the following app we have changed the attribute name on WorldClock from time to clock_time. We can make the app continue to work by changing the data_bind call to data_bind(clock_time=WorldClockApp.time):

    world_clock03.pyworld_clock01.tcssOutput
    from datetime import datetime\n\nfrom pytz import timezone\n\nfrom textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Digits, Label\n\n\nclass WorldClock(Widget):\n\n    clock_time: reactive[datetime] = reactive(datetime.now)\n\n    def __init__(self, timezone: str) -> None:\n        self.timezone = timezone\n        super().__init__()\n\n    def compose(self) -> ComposeResult:\n        yield Label(self.timezone)\n        yield Digits()\n\n    def watch_clock_time(self, time: datetime) -> None:\n        localized_time = time.astimezone(timezone(self.timezone))\n        self.query_one(Digits).update(localized_time.strftime(\"%H:%M:%S\"))\n\n\nclass WorldClockApp(App):\n    CSS_PATH = \"world_clock01.tcss\"\n\n    time: reactive[datetime] = reactive(datetime.now)\n\n    def compose(self) -> ComposeResult:\n        yield WorldClock(\"Europe/London\").data_bind(\n            clock_time=WorldClockApp.time  # (1)!\n        )\n        yield WorldClock(\"Europe/Paris\").data_bind(clock_time=WorldClockApp.time)\n        yield WorldClock(\"Asia/Tokyo\").data_bind(clock_time=WorldClockApp.time)\n\n    def update_time(self) -> None:\n        self.time = datetime.now()\n\n    def on_mount(self) -> None:\n        self.update_time()\n        self.set_interval(1, self.update_time)\n\n\nif __name__ == \"__main__\":\n    WorldClockApp().run()\n
    1. Uses keyword arguments to bind the time attribute of WorldClockApp to clock_time on WorldClock.
    Screen {\n    align: center middle;\n}\n\nWorldClock {\n    width: auto;\n    height: auto;\n    padding: 1 2;\n    background: $panel;\n    border: wide $background;\n\n    & Digits {\n        width: auto;\n        color: $secondary;\n    }\n}\n

    WorldClockApp \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/London\u258a \u258e\u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eEurope/Paris\u258a \u258e\u2576\u256e\u00a0\u256d\u2500\u2574\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u00a0\u2502\u00a0\u2570\u2500\u256e\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2576\u2534\u2574\u2576\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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 \u2581\u2581\u2581\u2581\u2581\u2581\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 \u258eAsia/Tokyo\u258a \u258e\u2576\u2500\u256e\u2576\u2500\u256e\u00a0\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256e\u256d\u2500\u2574\u258a \u258e\u250c\u2500\u2518\u250c\u2500\u2518\u00a0:\u00a0\u2570\u2500\u256e\u251c\u2500\u256e\u00a0:\u00a0\u00a0\u2500\u2524\u251c\u2500\u256e\u258a \u258e\u2570\u2500\u2574\u2570\u2500\u2574\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\u00a0\u00a0\u00a0\u2576\u2500\u256f\u2570\u2500\u256f\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

    "},{"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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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\u00a0Quit\u00a0\u258f^p\u00a0palette

    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) -> 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\u00a0Dashboard\u00a0\u00a0s\u00a0Settings\u00a0\u00a0h\u00a0Help\u00a0\u258f^p\u00a0palette

    ModesApp Settings\u00a0Screen \u00a0d\u00a0Dashboard\u00a0\u00a0s\u00a0Settings\u00a0\u00a0h\u00a0Help\u00a0\u258f^p\u00a0palette

    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/screens/#screen-events","title":"Screen events","text":"

    Textual will send a ScreenSuspend event to screens that have become inactive due to another screen being pushed, or switching via a mode.

    When a screen becomes active, Textual will send a ScreenResume event to the newly active screen.

    These events can be useful if you want to disable processing for a screen that is no longer visible, for example.

    "},{"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 is an async framework powered by Python's asyncio library. While Textual doesn't require a particular test framework, it must provide support for asyncio testing.

    You can use any test framework you are familiar with, but we will be using pytest along with the pytest-asyncio plugin in this chapter.

    By default, the pytest-asyncio plugin requires each async test to be decorated with @pytest.mark.asyncio. You can avoid having to add this marker to every async test by setting asyncio_mode = auto in your pytest configuration or by running pytest with the --asyncio-mode=auto option.

    "},{"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\u00a0Go\u00a0Red\u00a0\u00a0g\u00a0Go\u00a0Green\u00a0\u00a0b\u00a0Go\u00a0Blue\u00a0\u258f^p\u00a0palette

    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 \u2576\u2500\u256e\u00a0\u2576\u256e\u00a0\u2577\u00a0\u2577\u256d\u2500\u2574\u256d\u2500\u256e\u2576\u2500\u256e \u00a0\u2500\u2524\u00a0\u00a0\u2502\u00a0\u2570\u2500\u2524\u2570\u2500\u256e\u2570\u2500\u2524\u250c\u2500\u2518 \u2576\u2500\u256f.\u2576\u2534\u2574\u00a0\u00a0\u2575\u2576\u2500\u256f\u2576\u2500\u256f\u2570\u2500\u2574 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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/#focus-keybindings","title":"Focus & keybindings","text":"

    Widgets can have a list of associated key bindings, which let them call actions in response to key presses.

    A widget is able to handle key presses if it or one of its descendants has focus.

    Widgets aren't focusable by default. To allow a widget to be focused, we need to set can_focus=True when defining a widget subclass. Here's an example of a simple focusable widget:

    counter01.pycounter.tcssOutput counter01.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Static\n\n\nclass Counter(Static, can_focus=True):  # (1)!\n    \"\"\"A counter that can be incremented and decremented by pressing keys.\"\"\"\n\n    count = reactive(0)\n\n    def render(self) -> RenderResult:\n        return f\"Count: {self.count}\"\n\n\nclass CounterApp(App[None]):\n    CSS_PATH = \"counter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield Counter()\n        yield Counter()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = CounterApp()\n    app.run()\n
    1. Allow the widget to receive input focus.
    counter.tcss
    Counter {\n    background: $panel-darken-1;\n    padding: 1 2;\n    color: $text-muted;\n\n    &:focus {  /* (1)! */\n        background: $primary;\n        color: $text;\n        text-style: bold;\n        outline-left: thick $accent;\n    }\n}\n
    1. These styles are applied only when the widget has focus.

    CounterApp \u2588 \u2588Count:\u00a00 \u2588 Count:\u00a00 Count:\u00a00 \u258f^p\u00a0palette

    The app above contains three Counter widgets, which we can focus by clicking or using Tab and Shift+Tab.

    Now that our counter is focusable, let's add some keybindings to it to allow us to change the count using the keyboard. To do this, we add a BINDINGS class variable to Counter, with bindings for Up and Down. These new bindings are linked to the change_count action, which updates the count reactive attribute.

    With our bindings in place, we can now change the count of the currently focused counter using Up and Down.

    counter02.pycounter.tcssOutput counter02.py
    from textual.app import App, ComposeResult, RenderResult\nfrom textual.reactive import reactive\nfrom textual.widgets import Footer, Static\n\n\nclass Counter(Static, can_focus=True):\n    \"\"\"A counter that can be incremented and decremented by pressing keys.\"\"\"\n\n    BINDINGS = [\n        (\"up,k\", \"change_count(1)\", \"Increment\"),  # (1)!\n        (\"down,j\", \"change_count(-1)\", \"Decrement\"),\n    ]\n\n    count = reactive(0)\n\n    def render(self) -> RenderResult:\n        return f\"Count: {self.count}\"\n\n    def action_change_count(self, amount: int) -> None:  # (2)!\n        self.count += amount\n\n\nclass CounterApp(App[None]):\n    CSS_PATH = \"counter.tcss\"\n\n    def compose(self) -> ComposeResult:\n        yield Counter()\n        yield Counter()\n        yield Counter()\n        yield Footer()\n\n\nif __name__ == \"__main__\":\n    app = CounterApp()\n    app.run()\n
    1. Associates presses of Up or K with the change_count action, passing 1 as the argument to increment the count. The final argument (\"Increment\") is a user-facing label displayed in the footer when this binding is active.
    2. Called when the binding is triggered. Take care to add the action_ prefix to the method name.
    counter.tcss
    Counter {\n    background: $panel-darken-1;\n    padding: 1 2;\n    color: $text-muted;\n\n    &:focus {  /* (1)! */\n        background: $primary;\n        color: $text;\n        text-style: bold;\n        outline-left: thick $accent;\n    }\n}\n
    1. These styles are applied only when the widget has focus.

    CounterApp Count:\u00a01 \u2588 \u2588Count:\u00a0-2 \u2588 Count:\u00a00 \u00a0\u2191\u00a0Increment\u00a0\u00a0\u2193\u00a0Decrement\u00a0\u258f^p\u00a0palette

    "},{"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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    "},{"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 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

    "},{"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.

    This is also true for anything that could take a significant time (more than a few milliseconds) to complete. For instance, reading from a subprocess or doing compute heavy work.

    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 \u2576\u2500\u256e\u00a0\u2576\u256e\u00a0\u2577\u00a0\u2577\u256d\u2500\u2574\u256d\u2500\u256e\u2576\u2500\u256e \u00a0\u2500\u2524\u00a0\u00a0\u2502\u00a0\u2570\u2500\u2524\u2570\u2500\u256e\u2570\u2500\u2524\u250c\u2500\u2518 \u2576\u2500\u256f.\u2576\u2534\u2574\u00a0\u00a0\u2575\u2576\u2500\u256f\u2576\u2500\u256f\u2570\u2500\u2574 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\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, RenderResult\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) -> RenderResult:\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":"how-to/style-inline-apps/","title":"Style Inline Apps","text":"

    Version 0.55.0 of Textual added support for running apps inline (below the prompt). Running an inline app is as simple as adding inline=True to run().

    Your apps will typically run inline without modification, but you may want to make some tweaks for inline mode, which you can do with a little CSS. This How-To will explain how.

    Let's look at an inline app. The following app displays the the current time (and keeps it up to date).

    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(inline=True)  #  (1)!\n
    1. The inline=True runs the app inline.

    With Textual's default settings, this clock will be displayed in 5 lines; 3 for the digits and 2 for a top and bottom border.

    You can change the height or the border with CSS and the :inline pseudo-selector, which only matches rules in inline mode. Let's update this app to remove the default border, and increase the height:

    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        &:inline {\n            border: none;\n            height: 50vh;\n            Digits {\n                color: $success;\n            }\n        }\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(inline=True)\n

    The highlighted CSS targets online inline mode. By setting the height rule on Screen we can define how many lines the app should consume when it runs. Setting border: none removes the default border when running in inline mode.

    We've also added a rule to change the color of the clock when running inline.

    "},{"location":"how-to/style-inline-apps/#summary","title":"Summary","text":"

    Most apps will not require modification to run inline, but if you want to tweak the height and border you can write CSS that targets inline mode with the :inline pseudo-selector.

    "},{"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/hatch/","title":"Hatch","text":"

    The hatch style fills a widget's background with a repeating character for a pleasing textured effect.

    "},{"location":"styles/hatch/#syntax","title":"Syntax","text":"
    \nhatch: (<hatch> | CHARACTER) <color> [<percentage>]\n

    The hatch type can be specified with a constant, or a string. For example, cross for cross hatch, or \"T\" for a custom character.

    The color can be any Textual color value.

    An optional percentage can be used to set the opacity.

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

    An app to show a few hatch effects.

    Outputhatch.pyhatch.tcss

    HatchApp \u250c\u2500\u00a0cross\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0horizontal\u00a0\u2500\u2510\u250c\u2500\u00a0custom\u00a0\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u00a0right\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2502\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2573\u2502\u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2502TTTTTTTTTTTTTT\u2502\u2502\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2572\u2502\u2502\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2571\u2502 \u2514\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\u2518\u2514\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\u2518\u2514\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\nHATCHES = (\"cross\", \"horizontal\", \"custom\", \"left\", \"right\")\n\n\nclass HatchApp(App):\n    CSS_PATH = \"hatch.tcss\"\n\n    def compose(self) -> ComposeResult:\n        with Horizontal():\n            for hatch in HATCHES:\n                static = Static(classes=f\"hatch {hatch}\")\n                static.border_title = hatch\n                with Vertical():\n                    yield static\n\n\nif __name__ == \"__main__\":\n    app = HatchApp()\n    app.run()\n
    .hatch {\n    height: 1fr;\n    border: solid $secondary;\n\n    &.cross {\n        hatch: cross $success;\n    }\n    &.horizontal {\n        hatch: horizontal $success 80%;\n    }\n    &.custom {\n        hatch: \"T\" $success 60%;\n    }\n    &.left {\n        hatch: left $success 40%;\n    }\n    &.right {\n        hatch: right $success 20%;\n    }\n}\n
    "},{"location":"styles/hatch/#css","title":"CSS","text":"
    /* Red cross hatch */\nhatch: cross red;\n/* Right diagonals, 50% transparent green. */\nhatch: right green 50%;\n/* T custom character in 80% blue. **/\nhatch: \"T\" blue 80%;\n
    "},{"location":"styles/hatch/#python","title":"Python","text":"
    widget.styles.hatch = (\"cross\", \"red\")\nwidget.styles.hatch = (\"right\", \"rgba(0,255,0,128)\")\nwidget.styles.hatch = (\"T\", \"blue\")\n
    "},{"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.
    • margin 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 columns and 5 rows */\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 color 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 color 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 color 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-background-active to set the scrollbar background color when the scrollbar is being dragged.
    • scrollbar-background-hover to set the scrollbar background 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-background-hover to set the scrollbar background 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-background-active to set the scrollbar background 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-background-active to set the scrollbar background 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-background-hover to set the scrollbar background 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;.

    Bases: Widget

    A simple clickable button.

    Parameters:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None ButtonVariant

    The variant of the button.

    'default' str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/button/#textual.widgets.Button(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button(variant)","title":"variant","text":""},{"location":"widgets/button/#textual.widgets.Button(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button(tooltip)","title":"tooltip","text":""},{"location":"widgets/button/#textual.widgets.Button.active_effect_duration","title":"active_effect_duration instance-attribute","text":"
    active_effect_duration = 0.2\n

    Amount of time in seconds the button 'press' animation lasts.

    "},{"location":"widgets/button/#textual.widgets.Button.label","title":"label class-attribute instance-attribute","text":"
    label = label\n

    The text label that appears within the button.

    "},{"location":"widgets/button/#textual.widgets.Button.variant","title":"variant class-attribute instance-attribute","text":"
    variant = variant\n

    The variant name for the button.

    "},{"location":"widgets/button/#textual.widgets.Button.Pressed","title":"Pressed","text":"
    Pressed(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.Pressed.button","title":"button instance-attribute","text":"
    button = button\n

    The button that was pressed.

    "},{"location":"widgets/button/#textual.widgets.Button.Pressed.control","title":"control property","text":"
    control\n

    An alias for Pressed.button.

    This will be the same value as Pressed.button.

    "},{"location":"widgets/button/#textual.widgets.Button.action_press","title":"action_press","text":"
    action_press()\n

    Activate a press of the button.

    "},{"location":"widgets/button/#textual.widgets.Button.error","title":"error classmethod","text":"
    error(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'error' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.error(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.error(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.error(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.error(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.error(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.error(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.press","title":"press","text":"
    press()\n

    Animate the button and send the Pressed message.

    Can be used to simulate the button being pressed by a user.

    Returns:

    Type Description Self

    The button instance.

    "},{"location":"widgets/button/#textual.widgets.Button.success","title":"success classmethod","text":"
    success(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'success' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.success(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.success(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.success(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.success(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.success(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.success(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.validate_label","title":"validate_label","text":"
    validate_label(label)\n

    Parse markup for self.label

    "},{"location":"widgets/button/#textual.widgets.Button.warning","title":"warning classmethod","text":"
    warning(\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:

    Name Type Description Default TextType | None

    The text that appears within the button.

    None bool

    Whether the button is disabled or not.

    False str | None

    The name of the button.

    None str | None

    The ID of the button in the DOM.

    None str | None

    The CSS classes of the button.

    None bool

    Whether the button is disabled or not.

    False

    Returns:

    Type Description Button

    A Button widget of the 'warning' variant.

    "},{"location":"widgets/button/#textual.widgets.Button.warning(label)","title":"label","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(disabled)","title":"disabled","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(name)","title":"name","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(id)","title":"id","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(classes)","title":"classes","text":""},{"location":"widgets/button/#textual.widgets.Button.warning(disabled)","title":"disabled","text":""},{"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.

    Bases: ToggleButton

    A check box widget that represents a boolean value.

    Parameters:

    Name Type Description Default TextType

    The label for the toggle.

    '' bool

    The initial value of the toggle.

    False bool

    Should the button come before the label, or after?

    True str | None

    The name of the toggle.

    None str | None

    The ID of the toggle in the DOM.

    None str | None

    The CSS classes of the toggle.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    RenderableType | None = None,

    None"},{"location":"widgets/checkbox/#textual.widgets.Checkbox(label)","title":"label","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(value)","title":"value","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(button_first)","title":"button_first","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(name)","title":"name","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(id)","title":"id","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(classes)","title":"classes","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(disabled)","title":"disabled","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox(tooltip)","title":"tooltip","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed","title":"Changed","text":"
    Changed(toggle_button, value)\n

    Bases: Changed

    Posted when the value of the checkbox changes.

    This message can be handled using an on_checkbox_changed method.

    Parameters:

    Name Type Description Default ToggleButton

    The toggle button sending the message.

    required bool

    The value of the toggle button.

    required"},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed(toggle_button)","title":"toggle_button","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed(value)","title":"value","text":""},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed.checkbox","title":"checkbox property","text":"
    checkbox\n

    The checkbox that was changed.

    "},{"location":"widgets/checkbox/#textual.widgets.Checkbox.Changed.control","title":"control property","text":"
    control\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 Lady\u00a0Jessica 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\u2586\u2586 c\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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 Lady\u00a0Jessica 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\u00a0Collapse\u00a0All\u00a0\u00a0e\u00a0Expand\u00a0All\u00a0\u258f^p\u00a0palette

    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.

    Bases: Widget

    A collapsible container.

    Parameters:

    Name Type Description Default Widget

    Contents that will be collapsed/expanded.

    () str

    Title of the collapsed/expanded contents.

    'Toggle' bool

    Default status of the contents.

    True str

    Collapsed symbol before the title.

    '\u25b6' str

    Expanded symbol before the title.

    '\u25bc' str | None

    The name of the collapsible.

    None str | None

    The ID of the collapsible in the DOM.

    None str | None

    The CSS classes of the collapsible.

    None bool

    Whether the collapsible is disabled or not.

    False"},{"location":"widgets/collapsible/#textual.widgets.Collapsible(*children)","title":"*children","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(title)","title":"title","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(collapsed)","title":"collapsed","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(collapsed_symbol)","title":"collapsed_symbol","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(expanded_symbol)","title":"expanded_symbol","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(name)","title":"name","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(id)","title":"id","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(classes)","title":"classes","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible(disabled)","title":"disabled","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Collapsed","title":"Collapsed","text":"
    Collapsed(collapsible)\n

    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.

    Parameters:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Collapsed(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Expanded","title":"Expanded","text":"
    Expanded(collapsible)\n

    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.

    Parameters:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Expanded(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled","title":"Toggled","text":"
    Toggled(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:

    Name Type Description Default Collapsible

    The Collapsible widget that was toggled.

    required"},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled(collapsible)","title":"collapsible","text":""},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled.collapsible","title":"collapsible instance-attribute","text":"
    collapsible = collapsible\n

    The collapsible that was toggled.

    "},{"location":"widgets/collapsible/#textual.widgets.Collapsible.Toggled.control","title":"control property","text":"
    control\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\u2502 \u2502\u2502 \u2502Three\u00a0Flavours\u00a0Cornetto\u2502 \u2502\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u00a0of\u00a0\u2502 \u2502British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u2502 \u2502Shaun\u00a0of\u00a0the\u00a0Dead\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Strawberry\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502Hot\u00a0Fuzz\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Classico\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502The\u00a0World's\u00a0End\u2502 \u2502\u2502 \u2502\u2502 \u2502Flavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\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\u00a0\u2502 \u2502Mint\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02013-07-19\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2587\u2587\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.

    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:

    Name Type Description Default Widget

    The widgets to switch between.

    () str | None

    The name of the content switcher.

    None str | None

    The ID of the content switcher in the DOM.

    None str | None

    The CSS classes of the content switcher.

    None bool

    Whether the content switcher is disabled or not.

    False str | None

    The ID of the initial widget to show, None or empty string for the first tab.

    None Note

    If initial is not supplied no children will be shown to start with.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(*children)","title":"*children","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(name)","title":"name","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(id)","title":"id","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(classes)","title":"classes","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(disabled)","title":"disabled","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher(initial)","title":"initial","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
    current = reactive[Optional[str]](None, init=False)\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.ContentSwitcher.visible_content","title":"visible_content property","text":"
    visible_content\n

    A reference to the currently-visible widget.

    None if nothing is visible.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content","title":"add_content","text":"
    add_content(widget, *, id=None, set_current=False)\n

    Add new content to the ContentSwitcher.

    Parameters:

    Name Type Description Default Widget

    A Widget to add.

    required str | None

    ID for the widget, or None if the widget already has an ID.

    None bool

    Set the new widget as current (which will cause it to display).

    False

    Returns:

    Type Description AwaitComplete

    An awaitable to wait for the new content to be mounted.

    "},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(widget)","title":"widget","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(id)","title":"id","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.add_content(set_current)","title":"set_current","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current","title":"watch_current","text":"
    watch_current(old, new)\n

    React to the current visible child choice being changed.

    Parameters:

    Name Type Description Default str | None

    The old widget ID (or None if there was no widget).

    required str | None

    The new widget ID (or None if nothing should be shown).

    required"},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current(old)","title":"old","text":""},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher.watch_current(new)","title":"new","text":""},{"location":"widgets/data_table/","title":"DataTable","text":"

    A widget to display text in a table. This includes the ability to update data, use a cursor to navigate data, respond to mouse clicks, delete rows or columns, and individually render each cell as a Rich Text renderable. DataTable provides an efficiently displayed and updated table capable for most applications.

    Applications may have custom rules for formatting, numbers, repopulating tables after searching or filtering, and responding to selections. The widget emits events to interface with custom logic.

    • 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 may need to convert that coordinate to a cell key first using the coordinate_to_cell_key method.

    "},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

    A cursor allows navigating within a table with the keyboard or mouse. There are four cursor types: \"cell\" (the default), \"row\", \"column\", and \"none\".

    Change the cursor type by assigning to the cursor_type reactive attribute. The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute.

    Using the keyboard, arrow keys, Page Up, Page Down, Home and End move the cursor highlight, emitting a CellHighlighted message, then enter selects the cell, emitting a CellSelected message. If the cursor_type is row, then RowHighlighted and RowSelected are emitted, similarly for ColumnHighlighted and ColumnSelected.

    When moving the mouse over the table, a MouseMove event is emitted, the cell hovered over is styled, and the hover_coordinate reactive attribute is updated. Clicking the mouse then emits the CellHighlighted and CellSelected events.

    A new table starts with no cell highlighted, i.e., row and column are zero. You can force the first item to highlight with move_cursor(row=1, column=1). All row and column indexes start at one.

    Column CursorRow CursorCell CursorNo 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

    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\", \"none\"])\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
    "},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

    Cells can be updated 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 \u00a03\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a09\u00a0\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a012\u00a0\u00a0 \u00a05\u00a0\u00a0\u00a010\u00a0\u00a0\u00a015\u00a0\u00a0\u2581\u2581 \u00a06\u00a0\u00a0\u00a012\u00a0\u00a0\u00a018\u00a0\u00a0 \u00a07\u00a0\u00a0\u00a014\u00a0\u00a0\u00a021\u00a0\u00a0 \u00a08\u00a0\u00a0\u00a016\u00a0\u00a0\u00a024\u00a0\u00a0 \u00a09\u00a0\u00a0\u00a018\u00a0\u00a0\u00a027\u00a0\u00a0 \u00a010\u00a0\u00a020\u00a0\u00a0\u00a030\u00a0\u00a0 \u00a011\u00a0\u00a022\u00a0\u00a0\u00a033\u00a0\u00a0 \u00a012\u00a0\u00a024\u00a0\u00a0\u00a036\u00a0\u00a0 \u00a013\u00a0\u00a026\u00a0\u00a0\u00a039\u00a0\u00a0 \u00a014\u00a0\u00a028\u00a0\u00a0\u00a042\u00a0\u00a0 \u00a015\u00a0\u00a030\u00a0\u00a0\u00a045\u00a0\u00a0 \u00a016\u00a0\u00a032\u00a0\u00a0\u00a048\u00a0\u00a0 \u00a017\u00a0\u00a034\u00a0\u00a0\u00a051\u00a0\u00a0 \u00a018\u00a0\u00a036\u00a0\u00a0\u00a054\u00a0\u00a0 \u00a019\u00a0\u00a038\u00a0\u00a0\u00a057\u00a0\u00a0 \u00a020\u00a0\u00a040\u00a0\u00a0\u00a060\u00a0\u00a0 \u00a021\u00a0\u00a042\u00a0\u00a0\u00a063\u00a0\u00a0 \u00a022\u00a0\u00a044\u00a0\u00a0\u00a066\u00a0\u00a0 \u00a023\u00a0\u00a046\u00a0\u00a0\u00a069\u00a0\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 rows can be sorted using the sort method.

    There are three methods of using sort:

    • By Column. Pass columns in as parameters to sort by the natural order of one or more columns. Specify a column using either a ColumnKey instance or the key you supplied to add_column. For example, sort(\"country\", \"region\") would sort by country, and, when the country values are equal, by region.
    • By Key function. Pass a function as the key parameter to sort, similar to the key function parameter of Python's sorted built-in. The function will be called once per row with a tuple of all row values.
    • By both Column and Key function. You can specify which columns to include as parameters to your key function. For example, sort(\"hours\", \"rate\", key=lambda h, r: h*r) passes two values to the key function for each row.

    The reverse argument reverses the order of your sort. Note that correct sorting may require your key function to undo your formatting.

    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\u00a0Sort\u00a0By\u00a0Average\u00a0Time\u00a0\u00a0n\u00a0Sort\u00a0By\u00a0Last\u00a0Name\u00a0\u00a0c\u00a0Sort\u00a0By\u00a0Country\u00a0\u00a0d\u00a0S\u258f^p\u00a0palette

    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/#labeled-rows","title":"Labeled 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.

    Labeled 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 Style with 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. pageup Move one page up. pagedown Move one page down. ctrl+home Move to the top. ctrl+end Move to the bottom. home Move to the home position (leftmost column). end Move to the end position (rightmost column)."},{"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) if zebra_stripes. datatable--odd-row Target odd rows (row indices start at 0) if zebra_stripes.

    Bases: ScrollView, Generic[CellType]

    A tabular widget that contains data.

    Parameters:

    Name Type Description Default bool

    Whether the table header should be visible or not.

    True bool

    Whether the row labels should be shown or not.

    True int

    The number of rows, counting from the top, that should be fixed and still visible when the user scrolls down.

    0 int

    The number of columns, counting from the left, that should be fixed and still visible when the user scrolls right.

    0 bool

    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.

    False int

    The height, in number of cells, of the data table header.

    1 bool

    Whether the cursor should be visible when navigating the data table or not.

    True Literal['renderable', '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.

    'css' Literal['renderable', 'css']

    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.

    'renderable' CursorType

    The type of cursor to be used when navigating the data table with the keyboard.

    'cell' int

    The number of cells added on each side of each column. Setting this value to zero will likely make your table very hard to read.

    1 str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/data_table/#textual.widgets.DataTable(show_header)","title":"show_header","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(show_row_labels)","title":"show_row_labels","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(fixed_rows)","title":"fixed_rows","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(fixed_columns)","title":"fixed_columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(zebra_stripes)","title":"zebra_stripes","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(header_height)","title":"header_height","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(show_cursor)","title":"show_cursor","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_foreground_priority)","title":"cursor_foreground_priority","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_background_priority)","title":"cursor_background_priority","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cursor_type)","title":"cursor_type","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(cell_padding)","title":"cell_padding","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(name)","title":"name","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(id)","title":"id","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(classes)","title":"classes","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable(disabled)","title":"disabled","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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    Binding(\"ctrl+home\", \"scroll_top\", \"Top\", show=False),\n    Binding(\n        \"ctrl+end\", \"scroll_bottom\", \"Bottom\", show=False\n    ),\n    Binding(\"home\", \"scroll_home\", \"Home\", show=False),\n    Binding(\"end\", \"scroll_end\", \"End\", show=False),\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. pageup Move one page up. pagedown Move one page down. ctrl+home Move to the top. ctrl+end Move to the bottom. home Move to the home position (leftmost column). end Move to the end position (rightmost column)."},{"location":"widgets/data_table/#textual.widgets.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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) if zebra_stripes. datatable--odd-row Target odd rows (row indices start at 0) if zebra_stripes."},{"location":"widgets/data_table/#textual.widgets.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.DataTable.columns","title":"columns instance-attribute","text":"
    columns = {}\n

    Metadata about the columns of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets.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.DataTable.cursor_column","title":"cursor_column property","text":"
    cursor_column\n

    The index of the column that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.cursor_coordinate","title":"cursor_coordinate class-attribute instance-attribute","text":"
    cursor_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.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.DataTable.cursor_row","title":"cursor_row property","text":"
    cursor_row\n

    The index of the row that the DataTable cursor is currently on.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.cursor_type","title":"cursor_type class-attribute instance-attribute","text":"
    cursor_type = cursor_type\n

    The type of cursor of the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.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.DataTable.hover_column","title":"hover_column property","text":"
    hover_column\n

    The index of the column that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.hover_coordinate","title":"hover_coordinate class-attribute instance-attribute","text":"
    hover_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.DataTable.hover_row","title":"hover_row property","text":"
    hover_row\n

    The index of the row that the mouse cursor is currently hovering above.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ordered_columns","title":"ordered_columns property","text":"
    ordered_columns\n

    The list of Columns in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ordered_rows","title":"ordered_rows property","text":"
    ordered_rows\n

    The list of Rows in the DataTable, ordered as they appear on screen.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.row_count","title":"row_count property","text":"
    row_count\n

    The number of rows currently present in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.rows","title":"rows instance-attribute","text":"
    rows = {}\n

    Metadata about the rows of the table, indexed by their key.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.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.DataTable.zebra_stripes","title":"zebra_stripes class-attribute instance-attribute","text":"
    zebra_stripes = zebra_stripes\n

    Apply alternating styles, datatable--even-row and datatable-odd-row, to create a zebra effect, e.g., alternating light and dark backgrounds.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted","title":"CellHighlighted","text":"
    CellHighlighted(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.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
    cell_key = cell_key\n

    The key for the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
    coordinate = coordinate\n

    The coordinate of the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
    value = value\n

    The value in the highlighted cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected","title":"CellSelected","text":"
    CellSelected(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.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
    cell_key = cell_key\n

    The key for the selected cell.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
    coordinate = coordinate\n

    The coordinate of the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.CellSelected.value","title":"value instance-attribute","text":"
    value = value\n

    The value in the cell that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted","title":"ColumnHighlighted","text":"
    ColumnHighlighted(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.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.DataTable.ColumnHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column = cursor_column\n

    The x-coordinate of the column that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected","title":"ColumnSelected","text":"
    ColumnSelected(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.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.DataTable.ColumnSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
    cursor_column = cursor_column\n

    The x-coordinate of the column that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected","title":"HeaderSelected","text":"
    HeaderSelected(data_table, column_key, column_index, label)\n

    Bases: Message

    Posted when a column header/label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.DataTable.HeaderSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted","title":"RowHighlighted","text":"
    RowHighlighted(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.DataTable.RowHighlighted.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row = cursor_row\n

    The y-coordinate of the cursor that highlighted the row.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key of the row that was highlighted.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected","title":"RowLabelSelected","text":"
    RowLabelSelected(data_table, row_key, row_index, label)\n

    Bases: Message

    Posted when a row label is clicked.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
    label = label\n

    The text of the label.

    "},{"location":"widgets/data_table/#textual.widgets.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.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.DataTable.RowSelected","title":"RowSelected","text":"
    RowSelected(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.DataTable.RowSelected.control","title":"control property","text":"
    control\n

    Alias for the data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
    cursor_row = cursor_row\n

    The y-coordinate of the cursor that made the selection.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
    data_table = data_table\n

    The data table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
    row_key = row_key\n

    The key of the row that was selected.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the cursor one page down.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_left","title":"action_page_left","text":"
    action_page_left()\n

    Move the cursor one page left.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_right","title":"action_page_right","text":"
    action_page_right()\n

    Move the cursor one page right.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the cursor one page up.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_bottom","title":"action_scroll_bottom","text":"
    action_scroll_bottom()\n

    Move the cursor and scroll to the bottom.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_end","title":"action_scroll_end","text":"
    action_scroll_end()\n

    Move the cursor and scroll to the rightmost column.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_home","title":"action_scroll_home","text":"
    action_scroll_home()\n

    Move the cursor and scroll to the leftmost column.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.action_scroll_top","title":"action_scroll_top","text":"
    action_scroll_top()\n

    Move the cursor and scroll to the top.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column","title":"add_column","text":"
    add_column(label, *, width=None, key=None, default=None)\n

    Add a column to the table.

    Parameters:

    Name Type Description Default TextType

    A str or Text object containing the label (shown top of column).

    required int | None

    Width of the column in cells or None to fit content.

    None str | None

    A key which uniquely identifies this column. If None, it will be generated for you.

    None CellType | None

    The value to insert into pre-existing rows.

    None

    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.DataTable.add_column(label)","title":"label","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(width)","title":"width","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_column(default)","title":"default","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_columns","title":"add_columns","text":"
    add_columns(*labels)\n

    Add a number of columns.

    Parameters:

    Name Type Description Default 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.DataTable.add_columns(*labels)","title":"*labels","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row","title":"add_row","text":"
    add_row(*cells, height=1, key=None, label=None)\n

    Add a row at the bottom of the DataTable.

    Parameters:

    Name Type Description Default CellType

    Positional arguments should contain cell data.

    () int | None

    The height of a row (in lines). Use None to auto-detect the optimal height.

    1 str | None

    A key which uniquely identifies this row. If None, it will be generated for you and returned.

    None TextType | None

    The label for the row. Will be displayed to the left if supplied.

    None

    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.DataTable.add_row(*cells)","title":"*cells","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(height)","title":"height","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_row(label)","title":"label","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.add_rows","title":"add_rows","text":"
    add_rows(rows)\n

    Add a number of rows at the bottom of the DataTable.

    Parameters:

    Name Type Description Default Iterable[Iterable[CellType]]

    Iterable of rows. A row is an iterable of cells.

    required

    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.DataTable.add_rows(rows)","title":"rows","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.clear","title":"clear","text":"
    clear(columns=False)\n

    Clear the table.

    Parameters:

    Name Type Description Default bool

    Also clear the columns.

    False

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.clear(columns)","title":"columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key","text":"
    coordinate_to_cell_key(coordinate)\n

    Return the key for the cell currently occupying this coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to exam the current cell key of.

    required

    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.DataTable.coordinate_to_cell_key(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell","title":"get_cell","text":"
    get_cell(row_key, column_key)\n

    Given a row key and column key, return the value of the corresponding cell.

    Parameters:

    Name Type Description Default RowKey | str

    The row key of the cell.

    required ColumnKey | str

    The column key of the cell.

    required

    Returns:

    Type Description CellType

    The value of the cell identified by the row and column keys.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_at","title":"get_cell_at","text":"
    get_cell_at(coordinate)\n

    Get the value from the cell occupying the given coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to retrieve the value from.

    required

    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.DataTable.get_cell_at(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_coordinate","title":"get_cell_coordinate","text":"
    get_cell_coordinate(row_key, column_key)\n

    Given a row key and column key, return the corresponding cell coordinate.

    Parameters:

    Name Type Description Default RowKey | str

    The row key of the cell.

    required ColumnKey | str

    The column key of the cell.

    required

    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.DataTable.get_cell_coordinate(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_cell_coordinate(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column","title":"get_column","text":"
    get_column(column_key)\n

    Get the values from the column identified by the given column key.

    Parameters:

    Name Type Description Default ColumnKey | str

    The key of the column.

    required

    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.DataTable.get_column(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column_at","title":"get_column_at","text":"
    get_column_at(column_index)\n

    Get the values from the column at a given index.

    Parameters:

    Name Type Description Default int

    The index of the column.

    required

    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.DataTable.get_column_at(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_column_index","title":"get_column_index","text":"
    get_column_index(column_key)\n

    Return the current index for the column identified by column_key.

    Parameters:

    Name Type Description Default ColumnKey | str

    The column key to find the current index of.

    required

    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.DataTable.get_column_index(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row","title":"get_row","text":"
    get_row(row_key)\n

    Get the values from the row identified by the given row key.

    Parameters:

    Name Type Description Default RowKey | str

    The key of the row.

    required

    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.DataTable.get_row(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_at","title":"get_row_at","text":"
    get_row_at(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:

    Name Type Description Default int

    The index of the row.

    required

    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.DataTable.get_row_at(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_height","title":"get_row_height","text":"
    get_row_height(row_key)\n

    Given a row key, return the height of that row in terminal cells.

    Parameters:

    Name Type Description Default RowKey

    The key of the row.

    required

    Returns:

    Type Description int

    The height of the row, measured in terminal character cells.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_height(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.get_row_index","title":"get_row_index","text":"
    get_row_index(row_key)\n

    Return the current index for the row identified by row_key.

    Parameters:

    Name Type Description Default RowKey | str

    The row key to find the current index of.

    required

    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.DataTable.get_row_index(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_column_index","title":"is_valid_column_index","text":"
    is_valid_column_index(column_index)\n

    Return a boolean indicating whether the column_index is within table bounds.

    Parameters:

    Name Type Description Default int

    The column index to check.

    required

    Returns:

    Type Description bool

    True if the column index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_column_index(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_coordinate","title":"is_valid_coordinate","text":"
    is_valid_coordinate(coordinate)\n

    Return a boolean indicating whether the given coordinate is valid.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to validate.

    required

    Returns:

    Type Description bool

    True if the coordinate is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_coordinate(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_row_index","title":"is_valid_row_index","text":"
    is_valid_row_index(row_index)\n

    Return a boolean indicating whether the row_index is within table bounds.

    Parameters:

    Name Type Description Default int

    The row index to check.

    required

    Returns:

    Type Description bool

    True if the row index is within the bounds of the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.is_valid_row_index(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor","title":"move_cursor","text":"
    move_cursor(\n    *, row=None, column=None, animate=False, scroll=True\n)\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:

    Name Type Description Default int | None

    The new row to move the cursor to.

    None int | None

    The new column to move the cursor to.

    None bool

    Whether to animate the change of coordinates.

    False bool

    Scroll the cursor into view after moving.

    True"},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(row)","title":"row","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(column)","title":"column","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(animate)","title":"animate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.move_cursor(scroll)","title":"scroll","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_column","title":"refresh_column","text":"
    refresh_column(column_index)\n

    Refresh the column at the given index.

    Parameters:

    Name Type Description Default int

    The index of the column to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_column(column_index)","title":"column_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_coordinate","title":"refresh_coordinate","text":"
    refresh_coordinate(coordinate)\n

    Refresh the cell at a coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_coordinate(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_row","title":"refresh_row","text":"
    refresh_row(row_index)\n

    Refresh the row at the given index.

    Parameters:

    Name Type Description Default int

    The index of the row to refresh.

    required

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.refresh_row(row_index)","title":"row_index","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_column","title":"remove_column","text":"
    remove_column(column_key)\n

    Remove a column (identified by a key) from the DataTable.

    Parameters:

    Name Type Description Default ColumnKey | str

    The key identifying the column to remove.

    required

    Raises:

    Type Description ColumnDoesNotExist

    If the column key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_column(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_row","title":"remove_row","text":"
    remove_row(row_key)\n

    Remove a row (identified by a key) from the DataTable.

    Parameters:

    Name Type Description Default RowKey | str

    The key identifying the row to remove.

    required

    Raises:

    Type Description RowDoesNotExist

    If the row key does not exist.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.remove_row(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort","title":"sort","text":"
    sort(*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:

    Name Type Description Default ColumnKey | str

    One or more columns to sort by the values in.

    () Callable[[Any], Any] | None

    A function (or other callable) that returns a key to use for sorting purposes.

    None bool

    If True, the sort order will be reversed.

    False

    Returns:

    Type Description Self

    The DataTable instance.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(columns)","title":"columns","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(key)","title":"key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.sort(reverse)","title":"reverse","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell","title":"update_cell","text":"
    update_cell(\n    row_key, column_key, value, *, update_width=False\n)\n

    Update the cell identified by the specified row key and column key.

    Parameters:

    Name Type Description Default RowKey | str

    The key identifying the row.

    required ColumnKey | str

    The key identifying the column.

    required CellType

    The new value to put inside the cell.

    required bool

    Whether to resize the column width to accommodate for the new cell content.

    False

    Raises:

    Type Description CellDoesNotExist

    When the supplied row_key and column_key cannot be found in the table.

    "},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(row_key)","title":"row_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(column_key)","title":"column_key","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(value)","title":"value","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell(update_width)","title":"update_width","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at","title":"update_cell_at","text":"
    update_cell_at(coordinate, value, *, update_width=False)\n

    Update the content inside the cell currently occupying the given coordinate.

    Parameters:

    Name Type Description Default Coordinate

    The coordinate to update the cell at.

    required CellType

    The new value to place inside the cell.

    required bool

    Whether to resize the column width to accommodate for the new cell content.

    False"},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(coordinate)","title":"coordinate","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(value)","title":"value","text":""},{"location":"widgets/data_table/#textual.widgets.DataTable.update_cell_at(update_width)","title":"update_width","text":""},{"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","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","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\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\n

    The key of this cell's row.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column dataclass","text":"
    Column(\n    key, label, width=0, content_width=0, auto_width=False\n)\n

    Metadata for a column in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column.get_render_width","title":"get_render_width","text":"
    get_render_width(data_table)\n

    Width, in cells, required to render the column with padding included.

    Parameters:

    Name Type Description Default DataTable[Any]

    The data table where the column will be rendered.

    required

    Returns:

    Type Description int

    The width, in cells, required to render the column with padding included.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.Column.get_render_width(data_table)","title":"data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist","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","text":"
    ColumnKey(value=None)\n

    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","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 dataclass","text":"
    Row(key, height, label=None, auto_height=False)\n

    Metadata for a row in the DataTable.

    "},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist","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","text":"
    RowKey(value=None)\n

    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/data_table/#textual.widgets.data_table.StringKey","title":"StringKey","text":"
    StringKey(value=None)\n

    An object used as a key in a mapping.

    It can optionally wrap a string, and lookups into a map using the object behave the same as lookups using the string itself.

    "},{"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 and characters A-F are supported, in addition to +, -, ^, :, 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\u2576\u2500\u256e\u00a0\u2576\u256e\u00a0\u2577\u00a0\u2577\u2576\u256e\u00a0\u00a0\u256d\u2500\u2574\u256d\u2500\u256e\u2576\u2500\u256e\u00a0\u256d\u2500\u2574\u256d\u2500\u2574\u2576\u2500\u256e\u00a0\u256d\u2500\u2574\u256d\u2500\u256e\u256d\u2500\u256e\u2576\u2500\u2510\u2551 \u2551\u00a0\u2500\u2524\u00a0\u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0\u2502\u00a0\u00a0\u2570\u2500\u256e\u2570\u2500\u2524\u250c\u2500\u2518\u00a0\u251c\u2500\u256e\u2570\u2500\u256e\u00a0\u2500\u2524\u00a0\u2570\u2500\u256e\u251c\u2500\u2524\u2570\u2500\u2524\u00a0\u00a0\u2502\u2551 \u2551\u2576\u2500\u256f.\u2576\u2534\u2574\u00a0\u00a0\u2575\u2576\u2534\u2574,\u2576\u2500\u256f\u2576\u2500\u256f\u2570\u2500\u2574,\u2570\u2500\u256f\u2576\u2500\u256f\u2576\u2500\u256f,\u2576\u2500\u256f\u2570\u2500\u256f\u2576\u2500\u256f\u00a0\u00a0\u2575\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 \u2576\u256e\u00a0\u2577\u00a0\u2577\u00a0\u00a0\u00a0\u256d\u2500\u2574\u2576\u2500\u2510\u00a0\u00a0\u00a0\u256d\u2500\u256e\u256d\u2500\u2574 \u00a0\u2502\u00a0\u2570\u2500\u2524\u00a0:\u00a0\u2570\u2500\u256e\u00a0\u00a0\u2502\u00a0:\u00a0\u2502\u00a0\u2502\u2570\u2500\u256e \u2576\u2534\u2574\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2576\u2500\u256f\u00a0\u00a0\u2575\u00a0\u00a0\u00a0\u2570\u2500\u256f\u2576\u2500\u256f

    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(inline=True)\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.

    Bases: Widget

    A widget to display numerical values using a 3x3 grid of unicode characters.

    Parameters:

    Name Type Description Default str

    Value to display in widget.

    '' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/digits/#textual.widgets.Digits(value)","title":"value","text":""},{"location":"widgets/digits/#textual.widgets.Digits(name)","title":"name","text":""},{"location":"widgets/digits/#textual.widgets.Digits(id)","title":"id","text":""},{"location":"widgets/digits/#textual.widgets.Digits(classes)","title":"classes","text":""},{"location":"widgets/digits/#textual.widgets.Digits(disabled)","title":"disabled","text":""},{"location":"widgets/digits/#textual.widgets.Digits.value","title":"value property","text":"
    value\n

    The current value displayed in the Digits.

    "},{"location":"widgets/digits/#textual.widgets.Digits.update","title":"update","text":"
    update(value)\n

    Update the Digits with a new value.

    Parameters:

    Name Type Description Default str

    New value to display.

    required

    Raises:

    Type Description ValueError

    If the value isn't a str.

    "},{"location":"widgets/digits/#textual.widgets.Digits.update(value)","title":"value","text":""},{"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

    Bases: Tree[DirEntry]

    A Tree widget that presents files and directories.

    Parameters:

    Name Type Description Default str | Path

    Path to directory.

    required str | None

    The name of the widget, or None for no name.

    None str | None

    The ID of the widget in the DOM, or None for no ID.

    None str | None

    A space-separated list of classes, or None for no classes.

    None bool

    Whether the directory tree is disabled or not.

    False"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(name)","title":"name","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(id)","title":"id","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(classes)","title":"classes","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree(disabled)","title":"disabled","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.DirectoryTree.ICON_FILE","title":"ICON_FILE class-attribute instance-attribute","text":"
    ICON_FILE = '\ud83d\udcc4 '\n

    Unicode 'icon' to represent a file.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.PATH","title":"PATH class-attribute instance-attribute","text":"
    PATH = Path\n

    Callable that returns a fresh path object.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.path","title":"path class-attribute instance-attribute","text":"
    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.DirectoryTree.DirectorySelected","title":"DirectorySelected","text":"
    DirectorySelected(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:

    Name Type Description Default TreeNode[DirEntry]

    The tree node for the directory that was selected.

    required Path

    The path of the directory that was selected.

    required"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.control","title":"control property","text":"
    control\n

    The Tree that had a directory selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
    node = node\n

    The tree node of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
    path = path\n

    The path of the directory that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected","title":"FileSelected","text":"
    FileSelected(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:

    Name Type Description Default TreeNode[DirEntry]

    The tree node for the file that was selected.

    required Path

    The path of the file that was selected.

    required"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.control","title":"control property","text":"
    control\n

    The Tree that had a file selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
    node = node\n

    The tree node of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
    path = path\n

    The path of the file that was selected.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.clear_node","title":"clear_node","text":"
    clear_node(node)\n

    Clear all nodes under the given node.

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.filter_paths","title":"filter_paths","text":"
    filter_paths(paths)\n

    Filter the paths before adding them to the tree.

    Parameters:

    Name Type Description Default Iterable[Path]

    The paths to be filtered.

    required

    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.DirectoryTree.filter_paths(paths)","title":"paths","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.process_label","title":"process_label","text":"
    process_label(label)\n

    Process a str or Text into a label. May be overridden in a subclass to modify how labels are rendered.

    Parameters:

    Name Type Description Default TextType

    Label.

    required

    Returns:

    Type Description Text

    A Rich Text object.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.process_label(label)","title":"label","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload","title":"reload","text":"
    reload()\n

    Reload the DirectoryTree contents.

    Returns:

    Type Description AwaitComplete

    An optionally awaitable that ensures the tree has finished reloading.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload_node","title":"reload_node","text":"
    reload_node(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:

    Name Type Description Default TreeNode[DirEntry]

    The root of the subtree to reload.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable that ensures the subtree has finished reloading.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reload_node(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label","title":"render_label","text":"
    render_label(node, base_style, style)\n

    Render a label for the given node.

    Parameters:

    Name Type Description Default TreeNode[DirEntry]

    A tree node.

    required Style

    The base style of the widget.

    required Style

    The additional style for the label.

    required

    Returns:

    Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(base_style)","title":"base_style","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.render_label(style)","title":"style","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node","title":"reset_node","text":"
    reset_node(node, label, data=None)\n

    Clear the subtree and reset the given node.

    Parameters:

    Name Type Description Default TreeNode[DirEntry]

    The node to reset.

    required TextType

    The label for the node.

    required DirEntry | None

    Optional data for the node.

    None

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(node)","title":"node","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(label)","title":"label","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.reset_node(data)","title":"data","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.validate_path","title":"validate_path","text":"
    validate_path(path)\n

    Ensure that the path is of the Path type.

    Parameters:

    Name Type Description Default str | Path

    The path to validate.

    required

    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.DirectoryTree.validate_path(path)","title":"path","text":""},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree.watch_path","title":"watch_path async","text":"
    watch_path()\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":"

    Added in version 0.63.0

    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\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0del\u00a0Delete\u00a0the\u00a0thing\u00a0\u258f^p\u00a0palette

    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 compact bool False Display a more compact footer. show_command_palette bool True Display the key to invoke the command palette (show on the right hand side of 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":"

    This widget has no component classes.

    "},{"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.

    Bases: ScrollableContainer

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False bool

    Show key binding to command palette, on the right of the footer.

    True"},{"location":"widgets/footer/#textual.widgets.Footer(*children)","title":"*children","text":""},{"location":"widgets/footer/#textual.widgets.Footer(name)","title":"name","text":""},{"location":"widgets/footer/#textual.widgets.Footer(id)","title":"id","text":""},{"location":"widgets/footer/#textual.widgets.Footer(classes)","title":"classes","text":""},{"location":"widgets/footer/#textual.widgets.Footer(disabled)","title":"disabled","text":""},{"location":"widgets/footer/#textual.widgets.Footer(show_command_palette)","title":"show_command_palette","text":""},{"location":"widgets/footer/#textual.widgets.Footer.compact","title":"compact class-attribute instance-attribute","text":"
    compact = reactive(False)\n

    Display in compact style.

    "},{"location":"widgets/footer/#textual.widgets.Footer.show_command_palette","title":"show_command_palette class-attribute instance-attribute","text":"
    show_command_palette = reactive(True)\n

    Show the key to invoke the command palette.

    "},{"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.

    Bases: Widget

    A header widget with icon and clock.

    Parameters:

    Name Type Description Default bool

    True if the clock should be shown on the right of the header.

    False str | None

    The name of the header widget.

    None str | None

    The ID of the header widget in the DOM.

    None str | None

    The CSS classes of the header widget.

    None str | None

    Single character to use as an icon, or None for default.

    None str | None

    Time format (used by strftime) for clock, or None for default.

    None"},{"location":"widgets/header/#textual.widgets.Header(show_clock)","title":"show_clock","text":""},{"location":"widgets/header/#textual.widgets.Header(name)","title":"name","text":""},{"location":"widgets/header/#textual.widgets.Header(id)","title":"id","text":""},{"location":"widgets/header/#textual.widgets.Header(classes)","title":"classes","text":""},{"location":"widgets/header/#textual.widgets.Header(icon)","title":"icon","text":""},{"location":"widgets/header/#textual.widgets.Header(time_format)","title":"time_format","text":""},{"location":"widgets/header/#textual.widgets.Header.icon","title":"icon class-attribute instance-attribute","text":"
    icon = Reactive('\u2b58')\n

    A character for the icon at the top left.

    "},{"location":"widgets/header/#textual.widgets.Header.screen_sub_title","title":"screen_sub_title property","text":"
    screen_sub_title\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.screen_title","title":"screen_title property","text":"
    screen_title\n

    The title that this header will display.

    This depends on Screen.title and App.title.

    "},{"location":"widgets/header/#textual.widgets.Header.tall","title":"tall class-attribute instance-attribute","text":"
    tall = Reactive(False)\n

    Set to True for a taller header or False for a single line header.

    "},{"location":"widgets/header/#textual.widgets.Header.time_format","title":"time_format class-attribute instance-attribute","text":"
    time_format = Reactive('%X')\n

    Time format of the clock.

    "},{"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.

    Bases: Widget

    A text input widget.

    Parameters:

    Name Type Description Default str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Highlighter | None

    An optional highlighter for the input.

    None bool

    Flag to say if the field should obfuscate its content.

    False str | None

    A regex to restrict character inputs.

    None InputType

    The type of the input.

    'text' int

    The maximum length of the input, or 0 for no maximum length.

    0 Suggester | None

    Suggester associated with this input instance.

    None Validator | Iterable[Validator] | None

    An iterable of validators that the Input value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/input/#textual.widgets.Input(value)","title":"value","text":""},{"location":"widgets/input/#textual.widgets.Input(placeholder)","title":"placeholder","text":""},{"location":"widgets/input/#textual.widgets.Input(highlighter)","title":"highlighter","text":""},{"location":"widgets/input/#textual.widgets.Input(password)","title":"password","text":""},{"location":"widgets/input/#textual.widgets.Input(restrict)","title":"restrict","text":""},{"location":"widgets/input/#textual.widgets.Input(type)","title":"type","text":""},{"location":"widgets/input/#textual.widgets.Input(max_length)","title":"max_length","text":""},{"location":"widgets/input/#textual.widgets.Input(suggester)","title":"suggester","text":""},{"location":"widgets/input/#textual.widgets.Input(validators)","title":"validators","text":""},{"location":"widgets/input/#textual.widgets.Input(validate_on)","title":"validate_on","text":""},{"location":"widgets/input/#textual.widgets.Input(valid_empty)","title":"valid_empty","text":""},{"location":"widgets/input/#textual.widgets.Input(name)","title":"name","text":""},{"location":"widgets/input/#textual.widgets.Input(id)","title":"id","text":""},{"location":"widgets/input/#textual.widgets.Input(classes)","title":"classes","text":""},{"location":"widgets/input/#textual.widgets.Input(disabled)","title":"disabled","text":""},{"location":"widgets/input/#textual.widgets.Input(tooltip)","title":"tooltip","text":""},{"location":"widgets/input/#textual.widgets.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"left\",\n        \"cursor_left\",\n        \"Move cursor left\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+left\",\n        \"cursor_left_word\",\n        \"Move cursor left a word\",\n        show=False,\n    ),\n    Binding(\n        \"right\",\n        \"cursor_right\",\n        \"Move cursor right\",\n        show=False,\n    ),\n    Binding(\n        \"ctrl+right\",\n        \"cursor_right_word\",\n        \"Move cursor right a word\",\n        show=False,\n    ),\n    Binding(\n        \"backspace\",\n        \"delete_left\",\n        \"Delete character left\",\n        show=False,\n    ),\n    Binding(\n        \"home,ctrl+a\", \"home\", \"Go to start\", show=False\n    ),\n    Binding(\"end,ctrl+e\", \"end\", \"Go to end\", show=False),\n    Binding(\n        \"delete,ctrl+d\",\n        \"delete_right\",\n        \"Delete character 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.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.cursor_screen_offset","title":"cursor_screen_offset property","text":"
    cursor_screen_offset\n

    The offset of the cursor of this input in screen-space. (x, y)/(column, row)

    "},{"location":"widgets/input/#textual.widgets.Input.cursor_width","title":"cursor_width property","text":"
    cursor_width\n

    The width of the input (with extra space for cursor at the end).

    "},{"location":"widgets/input/#textual.widgets.Input.is_valid","title":"is_valid property","text":"
    is_valid\n

    Check if the value has passed validation.

    "},{"location":"widgets/input/#textual.widgets.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.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.suggester","title":"suggester instance-attribute","text":"
    suggester = suggester\n

    The suggester used to provide completions as the user types.

    "},{"location":"widgets/input/#textual.widgets.Input.type","title":"type class-attribute instance-attribute","text":"
    type = type\n

    The type of the input.

    "},{"location":"widgets/input/#textual.widgets.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.validate_on","title":"validate_on instance-attribute","text":"
    validate_on = (\n    _POSSIBLE_VALIDATE_ON_VALUES & set(validate_on)\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.Changed","title":"Changed dataclass","text":"
    Changed(input, value, validation_result=None)\n

    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.Changed.control","title":"control property","text":"
    control\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets.Input.Changed.input","title":"input instance-attribute","text":"
    input\n

    The Input widget that was changed.

    "},{"location":"widgets/input/#textual.widgets.Input.Changed.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result = 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.Changed.value","title":"value instance-attribute","text":"
    value\n

    The value that the input was changed to.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted","title":"Submitted dataclass","text":"
    Submitted(input, value, validation_result=None)\n

    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.Submitted.control","title":"control property","text":"
    control\n

    Alias for self.input.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted.input","title":"input instance-attribute","text":"
    input\n

    The Input widget that is being submitted.

    "},{"location":"widgets/input/#textual.widgets.Input.Submitted.validation_result","title":"validation_result class-attribute instance-attribute","text":"
    validation_result = 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.Submitted.value","title":"value instance-attribute","text":"
    value\n

    The value of the Input being submitted.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left()\n

    Move the cursor one position to the left.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_left_word","title":"action_cursor_left_word","text":"
    action_cursor_left_word()\n

    Move the cursor left to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_right","title":"action_cursor_right","text":"
    action_cursor_right()\n

    Accept an auto-completion or move the cursor one position to the right.

    "},{"location":"widgets/input/#textual.widgets.Input.action_cursor_right_word","title":"action_cursor_right_word","text":"
    action_cursor_right_word()\n

    Move the cursor right to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left","title":"action_delete_left","text":"
    action_delete_left()\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left_all","title":"action_delete_left_all","text":"
    action_delete_left_all()\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_left_word","title":"action_delete_left_word","text":"
    action_delete_left_word()\n

    Delete leftward of the cursor position to the start of a word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right","title":"action_delete_right","text":"
    action_delete_right()\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right_all","title":"action_delete_right_all","text":"
    action_delete_right_all()\n

    Delete the current character and all characters to the right of the cursor position.

    "},{"location":"widgets/input/#textual.widgets.Input.action_delete_right_word","title":"action_delete_right_word","text":"
    action_delete_right_word()\n

    Delete the current character and all rightward to the start of the next word.

    "},{"location":"widgets/input/#textual.widgets.Input.action_end","title":"action_end","text":"
    action_end()\n

    Move the cursor to the end of the input.

    "},{"location":"widgets/input/#textual.widgets.Input.action_home","title":"action_home","text":"
    action_home()\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/input/#textual.widgets.Input.action_submit","title":"action_submit async","text":"
    action_submit()\n

    Handle a submit action.

    Normally triggered by the user pressing Enter. This may also run any validators.

    "},{"location":"widgets/input/#textual.widgets.Input.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character)\n

    Check if the widget may consume the given key.

    As an input we are expecting to capture printable keys.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    required

    Returns:

    Type Description bool

    True if the widget may capture the key in it's Key message, or False if it won't.

    "},{"location":"widgets/input/#textual.widgets.Input.check_consume_key(key)","title":"key","text":""},{"location":"widgets/input/#textual.widgets.Input.check_consume_key(character)","title":"character","text":""},{"location":"widgets/input/#textual.widgets.Input.clear","title":"clear","text":"
    clear()\n

    Clear the input.

    "},{"location":"widgets/input/#textual.widgets.Input.insert_text_at_cursor","title":"insert_text_at_cursor","text":"
    insert_text_at_cursor(text)\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters:

    Name Type Description Default str

    New text to insert.

    required"},{"location":"widgets/input/#textual.widgets.Input.insert_text_at_cursor(text)","title":"text","text":""},{"location":"widgets/input/#textual.widgets.Input.restricted","title":"restricted","text":"
    restricted()\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.validate","title":"validate","text":"
    validate(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.

    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 \u258f^p\u00a0palette

    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.

    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.

    Parameters:

    Name Type Description Default Widget

    Child widgets.

    () str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/list_item/#textual.widgets.ListItem(*children)","title":"*children","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(name)","title":"name","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(id)","title":"id","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(classes)","title":"classes","text":""},{"location":"widgets/list_item/#textual.widgets.ListItem(disabled)","title":"disabled","text":""},{"location":"widgets/list_item/#textual.widgets.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 \u258f^p\u00a0palette

    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.

    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:

    Name Type Description Default ListItem

    The ListItems to display in the list.

    () int | None

    The index that should be highlighted when the list is first mounted.

    0 str | None

    The name of the widget.

    None str | None

    The unique ID of the widget used in CSS/query selection.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the ListView is disabled or not.

    False"},{"location":"widgets/list_view/#textual.widgets.ListView(*children)","title":"*children","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(initial_index)","title":"initial_index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(name)","title":"name","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(id)","title":"id","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(classes)","title":"classes","text":""},{"location":"widgets/list_view/#textual.widgets.ListView(disabled)","title":"disabled","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.ListView.highlighted_child","title":"highlighted_child property","text":"
    highlighted_child\n

    The currently highlighted ListItem, or None if nothing is highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.index","title":"index class-attribute instance-attribute","text":"
    index = reactive[Optional[int]](\n    0, always_update=True, init=False\n)\n

    The index of the currently highlighted item.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Highlighted","title":"Highlighted","text":"
    Highlighted(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.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.ListView.Highlighted.control","title":"control property","text":"
    control\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.ListView.Highlighted.item","title":"item instance-attribute","text":"
    item = item\n

    The highlighted item, if there is one highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
    list_view = list_view\n

    The view that contains the item highlighted.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Selected","title":"Selected","text":"
    Selected(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.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.ListView.Selected.control","title":"control property","text":"
    control\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.ListView.Selected.item","title":"item instance-attribute","text":"
    item = item\n

    The selected item.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
    list_view = list_view\n

    The view that contains the item selected.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Highlight the next item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Highlight the previous item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.action_select_cursor","title":"action_select_cursor","text":"
    action_select_cursor()\n

    Select the current item in the list.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.append","title":"append","text":"
    append(item)\n

    Append a new ListItem to the end of the ListView.

    Parameters:

    Name Type Description Default ListItem

    The ListItem to append.

    required

    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.ListView.append(item)","title":"item","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.clear","title":"clear","text":"
    clear()\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.ListView.extend","title":"extend","text":"
    extend(items)\n

    Append multiple new ListItems to the end of the ListView.

    Parameters:

    Name Type Description Default Iterable[ListItem]

    The ListItems to append.

    required

    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.ListView.extend(items)","title":"items","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.insert","title":"insert","text":"
    insert(index, items)\n

    Insert new ListItem(s) to specified index.

    Parameters:

    Name Type Description Default int

    index to insert new ListItem.

    required Iterable[ListItem]

    The ListItems to insert.

    required

    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.ListView.insert(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.insert(items)","title":"items","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.pop","title":"pop","text":"
    pop(index=None)\n

    Remove last ListItem from ListView or Remove ListItem from ListView by index

    Parameters:

    Name Type Description Default Optional[int]

    index of ListItem to remove from ListView

    None

    Returns:

    Type Description AwaitRemove

    An awaitable that yields control to the event loop until the DOM has been updated to reflect item being removed.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.pop(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.remove_items","title":"remove_items","text":"
    remove_items(indices)\n

    Remove ListItems from ListView by indices

    Parameters:

    Name Type Description Default Iterable[int]

    index(s) of ListItems to remove from ListView

    required

    Returns:

    Type Description AwaitRemove

    An awaitable object that waits for the direct children to be removed.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.remove_items(indices)","title":"indices","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.validate_index","title":"validate_index","text":"
    validate_index(index)\n

    Clamp the index to the valid range, or set to None if there's nothing to highlight.

    Parameters:

    Name Type Description Default int | None

    The index to clamp.

    required

    Returns:

    Type Description int | None

    The clamped index.

    "},{"location":"widgets/list_view/#textual.widgets.ListView.validate_index(index)","title":"index","text":""},{"location":"widgets/list_view/#textual.widgets.ListView.watch_index","title":"watch_index","text":"
    watch_index(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

    Tip

    Widgets have a loading reactive which you can use to temporarily replace your widget with a LoadingIndicator. See the Loading Indicator section in the Widgets guide for details.

    "},{"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.

    Bases: Widget

    Display an animated loading indicator.

    Parameters:

    Name Type Description Default str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(name)","title":"name","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(id)","title":"id","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(classes)","title":"classes","text":""},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator(disabled)","title":"disabled","text":""},{"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.

    Bases: ScrollView

    A widget to log text.

    Parameters:

    Name Type Description Default bool

    Enable highlighting.

    False int | None

    Maximum number of lines to display.

    None bool

    Scroll to end on new lines.

    True str | None

    The name of the text log.

    None str | None

    The ID of the text log in the DOM.

    None str | None

    The CSS classes of the text log.

    None bool

    Whether the text log is disabled or not.

    False"},{"location":"widgets/log/#textual.widgets.Log(highlight)","title":"highlight","text":""},{"location":"widgets/log/#textual.widgets.Log(max_lines)","title":"max_lines","text":""},{"location":"widgets/log/#textual.widgets.Log(auto_scroll)","title":"auto_scroll","text":""},{"location":"widgets/log/#textual.widgets.Log(name)","title":"name","text":""},{"location":"widgets/log/#textual.widgets.Log(id)","title":"id","text":""},{"location":"widgets/log/#textual.widgets.Log(classes)","title":"classes","text":""},{"location":"widgets/log/#textual.widgets.Log(disabled)","title":"disabled","text":""},{"location":"widgets/log/#textual.widgets.Log.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll = auto_scroll\n

    Automatically scroll to new lines.

    "},{"location":"widgets/log/#textual.widgets.Log.highlight","title":"highlight instance-attribute","text":"
    highlight = highlight\n

    Enable highlighting.

    "},{"location":"widgets/log/#textual.widgets.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.line_count","title":"line_count property","text":"
    line_count\n

    Number of lines of content.

    "},{"location":"widgets/log/#textual.widgets.Log.lines","title":"lines property","text":"
    lines\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.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines = max_lines\n

    Maximum number of lines to show

    "},{"location":"widgets/log/#textual.widgets.Log.clear","title":"clear","text":"
    clear()\n

    Clear the Log.

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\n

    Called by Textual when styles update.

    "},{"location":"widgets/log/#textual.widgets.Log.refresh_lines","title":"refresh_lines","text":"
    refresh_lines(y_start, line_count=1)\n

    Refresh one or more lines.

    Parameters:

    Name Type Description Default int

    First line to refresh.

    required int

    Total number of lines to refresh.

    1"},{"location":"widgets/log/#textual.widgets.Log.refresh_lines(y_start)","title":"y_start","text":""},{"location":"widgets/log/#textual.widgets.Log.refresh_lines(line_count)","title":"line_count","text":""},{"location":"widgets/log/#textual.widgets.Log.write","title":"write","text":"
    write(data, scroll_end=None)\n

    Write to the log.

    Parameters:

    Name Type Description Default str

    Data to write.

    required bool | None

    Scroll to the end after writing, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write(data)","title":"data","text":""},{"location":"widgets/log/#textual.widgets.Log.write(scroll_end)","title":"scroll_end","text":""},{"location":"widgets/log/#textual.widgets.Log.write_line","title":"write_line","text":"
    write_line(line)\n

    Write content on a new line.

    Parameters:

    Name Type Description Default str

    String to write to the log.

    required

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write_line(line)","title":"line","text":""},{"location":"widgets/log/#textual.widgets.Log.write_lines","title":"write_lines","text":"
    write_lines(lines, scroll_end=None)\n

    Write an iterable of lines.

    Parameters:

    Name Type Description Default Iterable[str]

    An iterable of strings to write.

    required bool | None

    Scroll to the end after writing, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The Log instance.

    "},{"location":"widgets/log/#textual.widgets.Log.write_lines(lines)","title":"lines","text":""},{"location":"widgets/log/#textual.widgets.Log.write_lines(scroll_end)","title":"scroll_end","text":""},{"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 Markdown\u00a0Document This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. Features 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

    Bases: Widget

    Parameters:

    Name Type Description Default str | None

    String containing Markdown or None to leave blank for now.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown/#textual.widgets.Markdown(markdown)","title":"markdown","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(name)","title":"name","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(id)","title":"id","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(classes)","title":"classes","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown(parser_factory)","title":"parser_factory","text":""},{"location":"widgets/markdown/#textual.widgets.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.code_dark_theme","title":"code_dark_theme class-attribute instance-attribute","text":"
    code_dark_theme = reactive('material')\n

    The theme to use for code blocks when in dark mode.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.code_light_theme","title":"code_light_theme class-attribute instance-attribute","text":"
    code_light_theme = reactive('material-light')\n

    The theme to use for code blocks when in light mode.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked","title":"LinkClicked","text":"
    LinkClicked(markdown, href)\n

    Bases: Message

    A link in the document was clicked.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked.control","title":"control property","text":"
    control\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.LinkClicked.href","title":"href instance-attribute","text":"
    href = unquote(href)\n

    The link that was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected","text":"
    TableOfContentsSelected(markdown, block_id)\n

    Bases: Message

    An item in the TOC was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
    block_id = block_id\n

    ID of the block that was selected.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsSelected.control","title":"control property","text":"
    control\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.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated","text":"
    TableOfContentsUpdated(markdown, table_of_contents)\n

    Bases: Message

    The table of contents was updated.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
    control\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.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget associated with the table of contents.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
    table_of_contents = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.goto_anchor","title":"goto_anchor","text":"
    goto_anchor(anchor)\n

    Try and find the given anchor in the current document.

    Parameters:

    Name Type Description Default str

    The anchor to try and find.

    required 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.goto_anchor(anchor)","title":"anchor","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.load","title":"load async","text":"
    load(path)\n

    Load a new Markdown document.

    Parameters:

    Name Type Description Default Path

    Path to the document.

    required

    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.load(path)","title":"path","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    sanitize_location(location)\n

    Given a location, break out the path and any anchor.

    Parameters:

    Name Type Description Default str

    The location to sanitize.

    required

    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.sanitize_location(location)","title":"location","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.unhandled_token","title":"unhandled_token","text":"
    unhandled_token(token)\n

    Process an unhandled token.

    Parameters:

    Name Type Description Default Token

    The MarkdownIt token to handle.

    required

    Returns:

    Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.unhandled_token(token)","title":"token","text":""},{"location":"widgets/markdown/#textual.widgets.Markdown.update","title":"update","text":"
    update(markdown)\n

    Update the document with new Markdown.

    Parameters:

    Name Type Description Default str

    A string containing Markdown.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object. Await this to ensure that all children have been mounted.

    "},{"location":"widgets/markdown/#textual.widgets.Markdown.update(markdown)","title":"markdown","text":""},{"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 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258aMarkdown\u00a0Viewer \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a \u258aFeatures \u258a \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 \u258aTables \u258a \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a \u258aName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Type\u00a0Default\u00a0Description\u00a0\u00a0\u00a0\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\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\u2501\u2501\u2501\u2501\u2501\u2501\u00a0 \u258ashow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258afixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258azebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u00a0\u00a0\u00a0\u00a0 \u258aheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258ashow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a \u258a \u258a \u258aCode\u00a0Blocks \u258a\u2585\u2585 \u258aCode\u00a0blocks\u00a0are\u00a0syntax\u00a0highlighted,\u00a0with\u00a0guidelines. \u258a \u258a \u258aclassListViewExample(App): \u258a\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u258a\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldListView(

    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

    Bases: VerticalScroll

    A Markdown viewer widget.

    Parameters:

    Name Type Description Default str | None

    String containing Markdown, or None to leave blank.

    None bool

    Show a table of contents in a sidebar.

    True str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(show_table_of_contents)","title":"show_table_of_contents","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer(parser_factory)","title":"parser_factory","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.document","title":"document property","text":"
    document\n

    The Markdown document widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
    table_of_contents\n

    The table of contents widget.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.NavigatorUpdated","title":"NavigatorUpdated","text":"
    NavigatorUpdated()\n

    Bases: Message

    Navigator has been changed (clicked link etc).

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.back","title":"back async","text":"
    back()\n

    Go back one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.forward","title":"forward async","text":"
    forward()\n

    Go forward one level in the history.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer.go","title":"go async","text":"
    go(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 = 'list[tuple[int, str, str | None]]'\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","text":"
    Markdown(\n    markdown=None,\n    *,\n    name=None,\n    id=None,\n    classes=None,\n    parser_factory=None\n)\n

    Bases: Widget

    Parameters:

    Name Type Description Default str | None

    String containing Markdown or None to leave blank for now.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None Callable[[], MarkdownIt] | None

    A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

    None"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown(parser_factory)","title":"parser_factory","text":""},{"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.code_dark_theme","title":"code_dark_theme class-attribute instance-attribute","text":"
    code_dark_theme = reactive('material')\n

    The theme to use for code blocks when in dark mode.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.code_light_theme","title":"code_light_theme class-attribute instance-attribute","text":"
    code_light_theme = reactive('material-light')\n

    The theme to use for code blocks when in light mode.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.LinkClicked","title":"LinkClicked","text":"
    LinkClicked(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\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 = unquote(href)\n

    The link that was selected.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
    markdown = markdown\n

    The Markdown widget containing the link clicked.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected","text":"
    TableOfContentsSelected(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 = 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\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\n

    The Markdown widget where the selected item is.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated","text":"
    TableOfContentsUpdated(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\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\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 = table_of_contents\n

    Table of contents.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.goto_anchor","title":"goto_anchor","text":"
    goto_anchor(anchor)\n

    Try and find the given anchor in the current document.

    Parameters:

    Name Type Description Default str

    The anchor to try and find.

    required 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.goto_anchor(anchor)","title":"anchor","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.load","title":"load async","text":"
    load(path)\n

    Load a new Markdown document.

    Parameters:

    Name Type Description Default Path

    Path to the document.

    required

    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.load(path)","title":"path","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
    sanitize_location(location)\n

    Given a location, break out the path and any anchor.

    Parameters:

    Name Type Description Default str

    The location to sanitize.

    required

    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.sanitize_location(location)","title":"location","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.unhandled_token","title":"unhandled_token","text":"
    unhandled_token(token)\n

    Process an unhandled token.

    Parameters:

    Name Type Description Default Token

    The MarkdownIt token to handle.

    required

    Returns:

    Type Description MarkdownBlock | None

    Either a widget to be added to the output, or None.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.unhandled_token(token)","title":"token","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.Markdown.update","title":"update","text":"
    update(markdown)\n

    Update the document with new Markdown.

    Parameters:

    Name Type Description Default str

    A string containing Markdown.

    required

    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.Markdown.update(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock","title":"MarkdownBlock","text":"
    MarkdownBlock(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":"
    action_link(href)\n

    Called on link click.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.build_from_token","title":"build_from_token","text":"
    build_from_token(token)\n

    Build the block content from its source token.

    This method allows the block to be rebuilt on demand, which is useful when the styles assigned to the Markdown.COMPONENT_CLASSES change.

    See https://github.com/Textualize/textual/issues/3464 for more information.

    Parameters:

    Name Type Description Default Token

    The token from which this block is built.

    required"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.build_from_token(token)","title":"token","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.notify_style_update","title":"notify_style_update","text":"
    notify_style_update()\n

    If CSS was reloaded, try to rebuild this block from its token.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownBlock.rebuild","title":"rebuild","text":"
    rebuild()\n

    Rebuild the content of the block if we have a source token.

    "},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents","title":"MarkdownTableOfContents","text":"
    MarkdownTableOfContents(\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:

    Name Type Description Default Markdown

    The Markdown document associated with this table of contents.

    required str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(markdown)","title":"markdown","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(name)","title":"name","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(id)","title":"id","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(classes)","title":"classes","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents(disabled)","title":"disabled","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.markdown","title":"markdown instance-attribute","text":"
    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","text":"
    rebuild_table_of_contents(table_of_contents)\n

    Rebuilds the tree representation of the table of contents data.

    Parameters:

    Name Type Description Default TableOfContentsType

    Table of contents.

    required"},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.rebuild_table_of_contents(table_of_contents)","title":"table_of_contents","text":""},{"location":"widgets/markdown_viewer/#textual.widgets.markdown.MarkdownTableOfContents.watch_table_of_contents","title":"watch_table_of_contents","text":"
    watch_table_of_contents(table_of_contents)\n

    Triggered when the table of contents changes.

    "},{"location":"widgets/masked_input/","title":"MaskedInput","text":"

    Added in version 0.80.0

    A masked input derived from Input, allowing to restrict user input and give visual aid via a simple template mask, which also acts as an implicit validator.

    • Focusable
    • Container
    "},{"location":"widgets/masked_input/#example","title":"Example","text":"

    The example below shows a masked input to ease entering a credit card number.

    Outputcheckbox.py

    MaskedInputApp Enter\u00a0a\u00a0valid\u00a0credit\u00a0card\u00a0number. \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 \u258a0000-0000-0000-0000\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, ComposeResult\nfrom textual.widgets import Label, MaskedInput\n\n\nclass MaskedInputApp(App):\n    # (1)!\n    CSS = \"\"\"\n    MaskedInput.-valid {\n        border: tall $success 60%;\n    }\n    MaskedInput.-valid:focus {\n        border: tall $success;\n    }\n    MaskedInput {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Label(\"Enter a valid credit card number.\")\n        yield MaskedInput(\n            template=\"9999-9999-9999-9999;0\",  # (2)!\n        )\n\n\napp = MaskedInputApp()\n\nif __name__ == \"__main__\":\n    app.run()\n
    "},{"location":"widgets/masked_input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description template str \"\" The template mask string."},{"location":"widgets/masked_input/#the-template-string-format","title":"The template string format","text":"

    A MaskedInput template length defines the maximum length of the input value. Each character of the mask defines a regular expression used to restrict what the user can insert in the corresponding position, and whether the presence of the character in the user input is required for the MaskedInput value to be considered valid, according to the following table:

    Mask character Regular expression Required? A [A-Za-z] Yes a [A-Za-z] No N [A-Za-z0-9] Yes n [A-Za-z0-9] No X [^ ] Yes x [^ ] No 9 [0-9] Yes 0 [0-9] No D [1-9] Yes d [1-9] No # [0-9+\\-] No H [A-Fa-f0-9] Yes h [A-Fa-f0-9] No B [0-1] Yes b [0-1] No

    There are some special characters that can be used to control automatic case conversion during user input: > converts all subsequent user input to uppercase; < to lowercase; ! disables automatic case conversion. Any other character that appears in the template mask is assumed to be a separator, which is a character that is automatically inserted when user reaches its position. All mask characters can be escaped by placing \\ in front of them, allowing any character to be used as separator. The mask can be terminated by ;c, where c is any character you want to be used as placeholder character. The placeholder parameter inherited by Input can be used to override this allowing finer grain tuning of the placeholder string.

    "},{"location":"widgets/masked_input/#messages","title":"Messages","text":"
    • MaskedInput.Changed
    • MaskedInput.Submitted
    "},{"location":"widgets/masked_input/#bindings","title":"Bindings","text":"

    The masked 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/masked_input/#component-classes","title":"Component Classes","text":"

    The masked 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).

    Bases: Input

    A masked text input widget.

    Parameters:

    Name Type Description Default str

    Template string.

    required str | None

    An optional default value for the input.

    None str

    Optional placeholder text for the input.

    '' Validator | Iterable[Validator] | None

    An iterable of validators that the MaskedInput value will be checked against.

    None Iterable[InputValidationOn] | 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.

    None bool

    Empty values are valid.

    False str | None

    Optional name for the masked input widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the input is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(template)","title":"template","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(value)","title":"value","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(placeholder)","title":"placeholder","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(validators)","title":"validators","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(validate_on)","title":"validate_on","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(valid_empty)","title":"valid_empty","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(name)","title":"name","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(id)","title":"id","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(classes)","title":"classes","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(disabled)","title":"disabled","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput(tooltip)","title":"tooltip","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.template","title":"template class-attribute instance-attribute","text":"
    template = template\n

    Input template mask currently in use.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left()\n

    Move the cursor one position to the left; separators are skipped.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_left_word","title":"action_cursor_left_word","text":"
    action_cursor_left_word()\n

    Move the cursor left next to the previous separator. If no previous separator is found, moves the cursor to the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_right","title":"action_cursor_right","text":"
    action_cursor_right()\n

    Move the cursor one position to the right; separators are skipped.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_cursor_right_word","title":"action_cursor_right_word","text":"
    action_cursor_right_word()\n

    Move the cursor right next to the next separator. If no next separator is found, moves the cursor to the end of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left","title":"action_delete_left","text":"
    action_delete_left()\n

    Delete one character to the left of the current cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left_all","title":"action_delete_left_all","text":"
    action_delete_left_all()\n

    Delete all characters to the left of the cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_left_word","title":"action_delete_left_word","text":"
    action_delete_left_word()\n

    Delete leftward of the cursor position to the previous separator or the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_right","title":"action_delete_right","text":"
    action_delete_right()\n

    Delete one character at the current cursor position.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_delete_right_word","title":"action_delete_right_word","text":"
    action_delete_right_word()\n

    Delete the current character and all rightward to next separator or the end of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.action_home","title":"action_home","text":"
    action_home()\n

    Move the cursor to the start of the input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.clear","title":"clear","text":"
    clear()\n

    Clear the masked input.

    "},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.insert_text_at_cursor","title":"insert_text_at_cursor","text":"
    insert_text_at_cursor(text)\n

    Insert new text at the cursor, move the cursor to the end of the new text.

    Parameters:

    Name Type Description Default str

    New text to insert.

    required"},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.insert_text_at_cursor(text)","title":"text","text":""},{"location":"widgets/masked_input/#textual.widgets.MaskedInput.validate","title":"validate","text":"
    validate(value)\n

    Run all the validators associated with this MaskedInput on the supplied value.

    Same as Input.validate() but also validates against template which acts as an additional implicit validator.

    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/masked_input/#textual.widgets.MaskedInput.validate_value","title":"validate_value","text":"
    validate_value(value)\n

    Validates value against template.

    "},{"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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators.

    Bases: ScrollView

    A vertical option list with bounce-bar highlighting.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\n    \"option-list--option\",\n    \"option-list--option-disabled\",\n    \"option-list--option-highlighted\",\n    \"option-list--option-hover\",\n    \"option-list--option-hover-highlighted\",\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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.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.OptionList.option_count","title":"option_count property","text":"
    option_count\n

    The count of options.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted","title":"OptionHighlighted","text":"
    OptionHighlighted(option_list, index)\n

    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.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionHighlighted(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage","title":"OptionMessage","text":"
    OptionMessage(option_list, index)\n

    Bases: Message

    Base class for all option messages.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.control","title":"control property","text":"
    control\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.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
    option = get_option_at_index(index)\n

    The highlighted option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
    option_id = id\n

    The ID of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
    option_index = index\n

    The index of the option that the message relates to.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
    option_list = option_list\n

    The option list that sent the message.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected","title":"OptionSelected","text":"
    OptionSelected(option_list, index)\n

    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.

    Parameters:

    Name Type Description Default OptionList

    The option list that owns the option.

    required int

    The index of the option that the message relates to.

    required"},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected(option_list)","title":"option_list","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.OptionSelected(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Move the highlight down to the next enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Move the highlight up to the previous enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_first","title":"action_first","text":"
    action_first()\n

    Move the highlight to the first enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_last","title":"action_last","text":"
    action_last()\n

    Move the highlight to the last enabled option.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the highlight down roughly by one page.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the highlight up roughly by one page.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.action_select","title":"action_select","text":"
    action_select()\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.OptionList.add_option","title":"add_option","text":"
    add_option(item=None)\n

    Add a new option to the end of the option list.

    Parameters:

    Name Type Description Default NewOptionListContent

    The new item to add.

    None

    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.OptionList.add_option(item)","title":"item","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.add_options","title":"add_options","text":"
    add_options(items)\n

    Add new options to the end of the option list.

    Parameters:

    Name Type Description Default Iterable[NewOptionListContent]

    The new items to add.

    required

    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.OptionList.add_options(items)","title":"items","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.clear_options","title":"clear_options","text":"
    clear_options()\n

    Clear the content of the option list.

    Returns:

    Type Description Self

    The OptionList instance.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option","title":"disable_option","text":"
    disable_option(option_id)\n

    Disable the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to disable.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.disable_option_at_index","title":"disable_option_at_index","text":"
    disable_option_at_index(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.OptionList.enable_option","title":"enable_option","text":"
    enable_option(option_id)\n

    Enable the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to enable.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.enable_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.enable_option_at_index","title":"enable_option_at_index","text":"
    enable_option_at_index(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.OptionList.get_option","title":"get_option","text":"
    get_option(option_id)\n

    Get the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to get.

    required

    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.OptionList.get_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.get_option_at_index","title":"get_option_at_index","text":"
    get_option_at_index(index)\n

    Get the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to get.

    required

    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.OptionList.get_option_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.get_option_index","title":"get_option_index","text":"
    get_option_index(option_id)\n

    Get the index of the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to get the index of.

    required

    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.OptionList.get_option_index(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option","title":"remove_option","text":"
    remove_option(option_id)\n

    Remove the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to remove.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.remove_option_at_index","title":"remove_option_at_index","text":"
    remove_option_at_index(index)\n

    Remove the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to remove.

    required

    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.OptionList.remove_option_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt","title":"replace_option_prompt","text":"
    replace_option_prompt(option_id, prompt)\n

    Replace the prompt of the option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the option to replace the prompt of.

    required RenderableType

    The new prompt for the option.

    required

    Returns:

    Type Description Self

    The OptionList instance.

    Raises:

    Type Description OptionDoesNotExist

    If no option has the given ID.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt(option_id)","title":"option_id","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index","text":"
    replace_option_prompt_at_index(index, prompt)\n

    Replace the prompt of the option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the option to replace the prompt of.

    required RenderableType

    The new prompt for the option.

    required

    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.OptionList.replace_option_prompt_at_index(index)","title":"index","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.replace_option_prompt_at_index(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.scroll_to_highlight","title":"scroll_to_highlight","text":"
    scroll_to_highlight(top=False)\n

    Ensure that the highlighted option is in view.

    Parameters:

    Name Type Description Default bool

    Scroll highlight to top of the list.

    False"},{"location":"widgets/option_list/#textual.widgets.OptionList.scroll_to_highlight(top)","title":"top","text":""},{"location":"widgets/option_list/#textual.widgets.OptionList.validate_highlighted","title":"validate_highlighted","text":"
    validate_highlighted(highlighted)\n

    Validate the highlighted property value on access.

    "},{"location":"widgets/option_list/#textual.widgets.OptionList.watch_highlighted","title":"watch_highlighted","text":"
    watch_highlighted(highlighted)\n

    React to the highlighted option having changed.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID","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","text":"
    Option(prompt, id=None, disabled=False)\n

    Class that holds the details of an individual option.

    Parameters:

    Name Type Description Default RenderableType

    The prompt for the option.

    required str | None

    The optional ID for the option.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"widgets/option_list/#textual.widgets.option_list.Option(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option(id)","title":"id","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option(disabled)","title":"disabled","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.Option.id","title":"id property","text":"
    id\n

    The optional ID for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option.prompt","title":"prompt property","text":"
    prompt\n

    The prompt for the option.

    "},{"location":"widgets/option_list/#textual.widgets.option_list.Option.set_prompt","title":"set_prompt","text":"
    set_prompt(prompt)\n

    Set the prompt for the option.

    Parameters:

    Name Type Description Default RenderableType

    The new prompt for the option.

    required"},{"location":"widgets/option_list/#textual.widgets.option_list.Option.set_prompt(prompt)","title":"prompt","text":""},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist","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","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.

    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:

    Name Type Description Default str | None

    The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

    None PlaceholderVariant

    The variant of the placeholder.

    'default' str | None

    The name of the placeholder.

    None str | None

    The ID of the placeholder in the DOM.

    None str | None

    A space separated string with the CSS classes of the placeholder, if any.

    None bool

    Whether the placeholder is disabled or not.

    False"},{"location":"widgets/placeholder/#textual.widgets.Placeholder(label)","title":"label","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(variant)","title":"variant","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(name)","title":"name","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(id)","title":"id","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(classes)","title":"classes","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder(disabled)","title":"disabled","text":""},{"location":"widgets/placeholder/#textual.widgets.Placeholder.variant","title":"variant class-attribute instance-attribute","text":"
    variant = validate_variant(variant)\n

    The current variant of the placeholder.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder.cycle_variant","title":"cycle_variant","text":"
    cycle_variant()\n

    Get the next variant in the cycle.

    Returns:

    Type Description Self

    The Placeholder instance.

    "},{"location":"widgets/placeholder/#textual.widgets.Placeholder.validate_variant","title":"validate_variant","text":"
    validate_variant(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.

    Bases: Widget

    A pretty-printing widget.

    Used to pretty-print any object.

    Parameters:

    Name Type Description Default Any

    The object to pretty-print.

    required str | None

    The name of the pretty widget.

    None str | None

    The ID of the pretty in the DOM.

    None str | None

    The CSS classes of the pretty.

    None"},{"location":"widgets/pretty/#textual.widgets.Pretty(object)","title":"object","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(name)","title":"name","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(id)","title":"id","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty(classes)","title":"classes","text":""},{"location":"widgets/pretty/#textual.widgets.Pretty.update","title":"update","text":"
    update(object)\n

    Update the content of the pretty widget.

    Parameters:

    Name Type Description Default Any

    The object to pretty-print.

    required"},{"location":"widgets/pretty/#textual.widgets.Pretty.update(object)","title":"object","text":""},{"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\u00a0Start\u00a0\u258f^p\u00a0palette

    IndeterminateProgressBar \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\u250139%00:00:07 \u00a0s\u00a0Start\u00a0\u258f^p\u00a0palette

    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\u00a0Start\u00a0\u258f^p\u00a0palette

    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/#gradient-bars","title":"Gradient Bars","text":"

    Progress bars support an optional gradient parameter, which renders a smooth gradient rather than a solid bar. To use a gradient, create and set a Gradient object on the ProgressBar widget.

    Note

    Setting a gradient will override styles set in CSS.

    Here's an example:

    Outputprogress_bar_gradient.py

    ProgressApp \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\u250170%--:--:--

    from textual.app import App, ComposeResult\nfrom textual.color import Gradient\nfrom textual.containers import Center, Middle\nfrom textual.widgets import ProgressBar\n\n\nclass ProgressApp(App[None]):\n    \"\"\"Progress bar with a rainbow gradient.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        gradient = Gradient.from_colors(\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        )\n        with Center():\n            with Middle():\n                yield ProgressBar(total=100, gradient=gradient)\n\n    def on_mount(self) -> None:\n        self.query_one(ProgressBar).update(progress=70)\n\n\nif __name__ == \"__main__\":\n    ProgressApp().run()\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\u00a0Start\u00a0\u258f^p\u00a0palette

    StyledProgressBar \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\u250139%00:00:07 \u00a0s\u00a0Start\u00a0\u258f^p\u00a0palette

    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\u00a0Start\u00a0\u258f^p\u00a0palette

    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 progress 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.

    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:

    Name Type Description Default float | None

    The total number of steps in the progress if known.

    None bool

    Whether to show the bar portion of the progress bar.

    True bool

    Whether to show the percentage status of the bar.

    True bool

    Whether to show the ETA countdown of the progress bar.

    True str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False Clock | None

    An optional clock object (leave as default unless testing).

    None Gradient | None

    An optional Gradient object (will replace CSS styles in the bar).

    None"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(total)","title":"total","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_bar)","title":"show_bar","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_percentage)","title":"show_percentage","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(show_eta)","title":"show_eta","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(name)","title":"name","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(id)","title":"id","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(classes)","title":"classes","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(disabled)","title":"disabled","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(clock)","title":"clock","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar(gradient)","title":"gradient","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.gradient","title":"gradient class-attribute instance-attribute","text":"
    gradient = reactive(None)\n

    Optional gradient object (will replace CSS styling in bar).

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.percentage","title":"percentage class-attribute instance-attribute","text":"
    percentage = reactive[Optional[float]](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.ProgressBar.progress","title":"progress class-attribute instance-attribute","text":"
    progress = reactive(0.0)\n

    The progress so far, in number of steps.

    "},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.total","title":"total class-attribute instance-attribute","text":"
    total = 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.ProgressBar.advance","title":"advance","text":"
    advance(advance=1)\n

    Advance the progress of the progress bar by the given amount.

    Example
    progress_bar.advance(10)  # Advance 10 steps.\n

    Parameters:

    Name Type Description Default float

    Number of steps to advance progress by.

    1"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.advance(advance)","title":"advance","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update","title":"update","text":"
    update(*, total=UNUSED, progress=UNUSED, advance=UNUSED)\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:

    Name Type Description Default None | float | UnusedParameter

    New total number of steps.

    UNUSED float | UnusedParameter

    Set the progress to the given number of steps.

    UNUSED float | UnusedParameter

    Advance the progress by this number of steps.

    UNUSED"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(total)","title":"total","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(progress)","title":"progress","text":""},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar.update(advance)","title":"advance","text":""},{"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

    Bases: ToggleButton

    A radio button widget that represents a boolean value.

    Note

    A RadioButton is best used within a RadioSet.

    Parameters:

    Name Type Description Default TextType

    The label for the toggle.

    '' bool

    The initial value of the toggle.

    False bool

    Should the button come before the label, or after?

    True str | None

    The name of the toggle.

    None str | None

    The ID of the toggle in the DOM.

    None str | None

    The CSS classes of the toggle.

    None bool

    Whether the button is disabled or not.

    False RenderableType | None

    RenderableType | None = None,

    None"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(label)","title":"label","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(value)","title":"value","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(button_first)","title":"button_first","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(name)","title":"name","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(id)","title":"id","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(classes)","title":"classes","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(disabled)","title":"disabled","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton(tooltip)","title":"tooltip","text":""},{"location":"widgets/radiobutton/#textual.widgets.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.RadioButton.Changed","title":"Changed","text":"
    Changed(toggle_button, value)\n

    Bases: Changed

    Posted when the value of the radio button changes.

    This message can be handled using an on_radio_button_changed method.

    Parameters:

    Name Type Description Default ToggleButton

    The toggle button sending the message.

    required bool

    The value of the toggle button.

    required"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed(toggle_button)","title":"toggle_button","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed(value)","title":"value","text":""},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed.control","title":"control property","text":"
    control\n

    Alias for Changed.radio_button.

    "},{"location":"widgets/radiobutton/#textual.widgets.RadioButton.Changed.radio_button","title":"radio_button property","text":"
    radio_button\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

    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:

    Name Type Description Default str | RadioButton

    The labels or RadioButtons to group together.

    () str | None

    The name of the radio set.

    None str | None

    The ID of the radio set in the DOM.

    None str | None

    The CSS classes of the radio set.

    None bool

    Whether the radio set is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None Note

    When a str label is provided, a RadioButton will be created from it.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet(buttons)","title":"buttons","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(name)","title":"name","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(id)","title":"id","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(classes)","title":"classes","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(disabled)","title":"disabled","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet(tooltip)","title":"tooltip","text":""},{"location":"widgets/radioset/#textual.widgets.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"down,right\",\n        \"next_button\",\n        \"Next option\",\n        show=False,\n    ),\n    Binding(\n        \"enter,space\", \"toggle_button\", \"Toggle\", show=False\n    ),\n    Binding(\n        \"up,left\",\n        \"previous_button\",\n        \"Previous option\",\n        show=False,\n    ),\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.RadioSet.pressed_button","title":"pressed_button property","text":"
    pressed_button\n

    The currently-pressed RadioButton, or None if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet.pressed_index","title":"pressed_index property","text":"
    pressed_index\n

    The index of the currently-pressed RadioButton, or -1 if none are pressed.

    "},{"location":"widgets/radioset/#textual.widgets.RadioSet.Changed","title":"Changed","text":"
    Changed(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:

    Name Type Description Default RadioButton

    The radio button that was pressed.

    required"},{"location":"widgets/radioset/#textual.widgets.RadioSet.Changed(pressed)","title":"pressed","text":""},{"location":"widgets/radioset/#textual.widgets.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.RadioSet.Changed.control","title":"control property","text":"
    control\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.RadioSet.Changed.index","title":"index instance-attribute","text":"
    index = pressed_index\n

    The index of the RadioButton that was pressed to make the change.

    "},{"location":"widgets/radioset/#textual.widgets.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.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.RadioSet.action_next_button","title":"action_next_button","text":"
    action_next_button()\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.RadioSet.action_previous_button","title":"action_previous_button","text":"
    action_previous_button()\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.RadioSet.action_toggle_button","title":"action_toggle_button","text":"
    action_toggle_button()\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.

    Bases: ScrollView

    A widget for logging Rich renderables and text.

    Parameters:

    Name Type Description Default int | None

    Maximum number of lines in the log or None for no maximum.

    None int

    Width to use for calls to write with no specified width.

    78 bool

    Enable word wrapping (default is off).

    False bool

    Automatically highlight content. By default, the ReprHighlighter is used. To customize highlighting, set highlight=True and then set the highlighter attribute to an instance of Highlighter.

    False bool

    Apply Rich console markup.

    False bool

    Enable automatic scrolling to end.

    True str | None

    The name of the text log.

    None str | None

    The ID of the text log in the DOM.

    None str | None

    The CSS classes of the text log.

    None bool

    Whether the text log is disabled or not.

    False"},{"location":"widgets/rich_log/#textual.widgets.RichLog(max_lines)","title":"max_lines","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(min_width)","title":"min_width","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(wrap)","title":"wrap","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(highlight)","title":"highlight","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(markup)","title":"markup","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(auto_scroll)","title":"auto_scroll","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(name)","title":"name","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(id)","title":"id","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(classes)","title":"classes","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog(disabled)","title":"disabled","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
    auto_scroll = auto_scroll\n

    Automatically scroll to the end on write.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.highlight","title":"highlight class-attribute instance-attribute","text":"
    highlight = highlight\n

    Automatically highlight content.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.highlighter","title":"highlighter instance-attribute","text":"
    highlighter = ReprHighlighter()\n

    Rich Highlighter used to highlight content when highlight is True

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.lines","title":"lines instance-attribute","text":"
    lines = []\n

    The lines currently visible in the log.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.markup","title":"markup class-attribute instance-attribute","text":"
    markup = markup\n

    Apply Rich console markup.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.max_lines","title":"max_lines class-attribute instance-attribute","text":"
    max_lines = max_lines\n

    Maximum number of lines in the log or None for no maximum.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.min_width","title":"min_width class-attribute instance-attribute","text":"
    min_width = min_width\n

    Minimum width of renderables.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.wrap","title":"wrap class-attribute instance-attribute","text":"
    wrap = wrap\n

    Enable word wrapping.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.clear","title":"clear","text":"
    clear()\n

    Clear the text log.

    Returns:

    Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.write","title":"write","text":"
    write(\n    content,\n    width=None,\n    expand=False,\n    shrink=True,\n    scroll_end=None,\n)\n

    Write a string or a Rich renderable to the bottom of the log.

    Notes

    The rendering of content will be deferred until the size of the RichLog is known. This means if you call write in compose or on_mount, the content will not be rendered immediately.

    Parameters:

    Name Type Description Default RenderableType | object

    Rich renderable (or a string).

    required int | None

    Width to render, or None to use RichLog.min_width. If specified, expand and shrink will be ignored.

    None bool

    Permit expanding of content to the width of the content region of the RichLog. If width is specified, then expand will be ignored.

    False bool

    Permit shrinking of content to fit within the content region of the RichLog. If width is specified, then shrink will be ignored.

    True bool | None

    Enable automatic scroll to end, or None to use self.auto_scroll.

    None

    Returns:

    Type Description Self

    The RichLog instance.

    "},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(content)","title":"content","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(width)","title":"width","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(expand)","title":"expand","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(shrink)","title":"shrink","text":""},{"location":"widgets/rich_log/#textual.widgets.RichLog.write(scroll_end)","title":"scroll_end","text":""},{"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.

    Bases: Widget

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

    Parameters:

    Name Type Description Default RuleOrientation

    The orientation of the rule.

    'horizontal' LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/rule/#textual.widgets.Rule(orientation)","title":"orientation","text":""},{"location":"widgets/rule/#textual.widgets.Rule(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule(disabled)","title":"disabled","text":""},{"location":"widgets/rule/#textual.widgets.Rule.line_style","title":"line_style class-attribute instance-attribute","text":"
    line_style = line_style\n

    The line style of the rule.

    "},{"location":"widgets/rule/#textual.widgets.Rule.orientation","title":"orientation class-attribute instance-attribute","text":"
    orientation = orientation\n

    The orientation of the rule.

    "},{"location":"widgets/rule/#textual.widgets.Rule.horizontal","title":"horizontal classmethod","text":"
    horizontal(\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:

    Name Type Description Default LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Returns:

    Type Description Rule

    A rule widget with horizontal orientation.

    "},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule.horizontal(disabled)","title":"disabled","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical","title":"vertical classmethod","text":"
    vertical(\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:

    Name Type Description Default LineStyle

    The line style of the rule.

    'solid' str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes of the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Returns:

    Type Description Rule

    A rule widget with vertical orientation.

    "},{"location":"widgets/rule/#textual.widgets.Rule.vertical(line_style)","title":"line_style","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(name)","title":"name","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(id)","title":"id","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(classes)","title":"classes","text":""},{"location":"widgets/rule/#textual.widgets.Rule.vertical(disabled)","title":"disabled","text":""},{"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","text":"

    Bases: Exception

    Exception raised for an invalid rule line style.

    "},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation","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.

    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:

    Name Type Description Default Iterable[tuple[RenderableType, SelectType]]

    Options to select from. If no options are provided then allow_blank must be set to True.

    required str

    Text to show in the control when no option is selected.

    'Select' bool

    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.

    True SelectType | NoSelection

    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.

    BLANK str | None

    The name of the select control.

    None str | None

    The ID of the control in the DOM.

    None str | None

    The CSS classes of the control.

    None bool

    Whether the control is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None

    Raises:

    Type Description EmptySelectError

    If no options are provided and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.Select(options)","title":"options","text":""},{"location":"widgets/select/#textual.widgets.Select(prompt)","title":"prompt","text":""},{"location":"widgets/select/#textual.widgets.Select(allow_blank)","title":"allow_blank","text":""},{"location":"widgets/select/#textual.widgets.Select(value)","title":"value","text":""},{"location":"widgets/select/#textual.widgets.Select(name)","title":"name","text":""},{"location":"widgets/select/#textual.widgets.Select(id)","title":"id","text":""},{"location":"widgets/select/#textual.widgets.Select(classes)","title":"classes","text":""},{"location":"widgets/select/#textual.widgets.Select(disabled)","title":"disabled","text":""},{"location":"widgets/select/#textual.widgets.Select(tooltip)","title":"tooltip","text":""},{"location":"widgets/select/#textual.widgets.Select.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"enter,down,space,up\",\n        \"show_overlay\",\n        \"Show menu\",\n        show=False,\n    )\n]\n
    Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets.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.expanded","title":"expanded class-attribute instance-attribute","text":"
    expanded = var(False, init=False)\n

    True to show the overlay, otherwise False.

    "},{"location":"widgets/select/#textual.widgets.Select.prompt","title":"prompt class-attribute instance-attribute","text":"
    prompt = prompt\n

    The prompt to show when no value is selected.

    "},{"location":"widgets/select/#textual.widgets.Select.value","title":"value class-attribute instance-attribute","text":"
    value = var[Union[SelectType, NoSelection]](\n    BLANK, init=False\n)\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.Changed","title":"Changed","text":"
    Changed(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.Changed.control","title":"control property","text":"
    control\n

    The Select that sent the message.

    "},{"location":"widgets/select/#textual.widgets.Select.Changed.select","title":"select instance-attribute","text":"
    select = select\n

    The select widget.

    "},{"location":"widgets/select/#textual.widgets.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.action_show_overlay","title":"action_show_overlay","text":"
    action_show_overlay()\n

    Show the overlay.

    "},{"location":"widgets/select/#textual.widgets.Select.clear","title":"clear","text":"
    clear()\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.from_values","title":"from_values classmethod","text":"
    from_values(\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:

    Name Type Description Default Iterable[SelectType]

    Values used to generate options to select from.

    required str

    Text to show in the control when no option is selected.

    'Select' bool

    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.

    True SelectType | NoSelection

    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.

    BLANK str | None

    The name of the select control.

    None str | None

    The ID of the control in the DOM.

    None str | None

    The CSS classes of the control.

    None bool

    Whether the control is disabled or not.

    False

    Returns:

    Type Description Select[SelectType]

    A new Select widget with the provided values as options.

    "},{"location":"widgets/select/#textual.widgets.Select.from_values(values)","title":"values","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(prompt)","title":"prompt","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(allow_blank)","title":"allow_blank","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(value)","title":"value","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(name)","title":"name","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(id)","title":"id","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(classes)","title":"classes","text":""},{"location":"widgets/select/#textual.widgets.Select.from_values(disabled)","title":"disabled","text":""},{"location":"widgets/select/#textual.widgets.Select.is_blank","title":"is_blank","text":"
    is_blank()\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.set_options","title":"set_options","text":"
    set_options(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:

    Name Type Description Default Iterable[tuple[RenderableType, SelectType]]

    An iterable of tuples containing the renderable to display for each option and the corresponding internal value.

    required

    Raises:

    Type Description EmptySelectError

    If the options iterable is empty and allow_blank is False.

    "},{"location":"widgets/select/#textual.widgets.Select.set_options(options)","title":"options","text":""},{"location":"widgets/select/#textual.widgets.select.EmptySelectError","title":"EmptySelectError","text":"

    Bases: Exception

    Raised when a Select has no options and allow_blank=False.

    "},{"location":"widgets/select/#textual.widgets.select.InvalidSelectValueError","title":"InvalidSelectValueError","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 Text) 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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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 \u258f^p\u00a0palette

    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":"
    • SelectionList.SelectionHighlighted
    • SelectionList.SelectionToggled
    • 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-hover Target an option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--separator Target the separators.

    Bases: Generic[SelectionType], OptionList

    A vertical selection list that allows making multiple selections.

    Parameters:

    Name Type Description Default Selection[SelectionType] | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

    The content for the selection list.

    () str | None

    The name of the selection list.

    None str | None

    The ID of the selection list in the DOM.

    None str | None

    The CSS classes of the selection list.

    None bool

    Whether the selection list is disabled or not.

    False"},{"location":"widgets/selection_list/#textual.widgets.SelectionList(*selections)","title":"*selections","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(name)","title":"name","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(id)","title":"id","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(classes)","title":"classes","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList(disabled)","title":"disabled","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\n    Binding(\"space\", \"select\", \"Toggle option\", show=False)\n]\n
    Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.SelectionList.selected","title":"selected property","text":"
    selected\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.SelectionList.SelectedChanged","title":"SelectedChanged dataclass","text":"
    SelectedChanged(selection_list)\n

    Bases: Generic[MessageSelectionType], Message

    Message sent when the collection of selected values changes.

    This is sent regardless of whether the change occurred via user interaction or programmatically the the SelectionList API.

    When a bulk change occurs, such as through select_all or deselect_all, only a single SelectedChanged message will be sent (rather than one per option).

    Can be handled using on_selection_list_selected_changed in a subclass of SelectionList or in a parent node in the DOM.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectedChanged.control","title":"control property","text":"
    control\n

    An alias for selection_list.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
    selection_list\n

    The SelectionList that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted","title":"SelectionHighlighted","text":"
    SelectionHighlighted(selection_list, index)\n

    Bases: SelectionMessage[MessageSelectionType]

    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.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionHighlighted(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage","title":"SelectionMessage","text":"
    SelectionMessage(selection_list, index)\n

    Bases: Generic[MessageSelectionType], Message

    Base class for all selection messages.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.control","title":"control property","text":"
    control\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.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
    selection = get_option_at_index(index)\n

    The highlighted selection.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
    selection_index = index\n

    The index of the selection that the message relates to.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
    selection_list = selection_list\n

    The selection list that sent the message.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled","title":"SelectionToggled","text":"
    SelectionToggled(selection_list, index)\n

    Bases: SelectionMessage[MessageSelectionType]

    Message sent when a selection is toggled.

    This is only sent when the value is explicitly toggled e.g. via toggle or toggle_all, or via user interaction. If you programmatically set a value to be selected, this message will not be sent, even if it happens to be the opposite of what was originally selected (i.e. setting a True to a False or vice-versa).

    Since this message indicates a toggle occurring at a per-option level, a message will be sent for each option that is toggled, even when a bulk action is performed (e.g. via toggle_all).

    Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

    Parameters:

    Name Type Description Default SelectionList[MessageSelectionType]

    The selection list that owns the selection.

    required int

    The index of the selection that the message relates to.

    required"},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled(selection_list)","title":"selection_list","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.SelectionToggled(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.add_option","title":"add_option","text":"
    add_option(item=None)\n

    Add a new selection option to the end of the list.

    Parameters:

    Name Type Description Default NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

    The new item to add.

    None

    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.SelectionList.add_option(item)","title":"item","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.add_options","title":"add_options","text":"
    add_options(items)\n

    Add new selection options to the end of the list.

    Parameters:

    Name Type Description Default Iterable[NewOptionListContent | Selection[SelectionType] | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]]

    The new items to add.

    required

    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.SelectionList.add_options(items)","title":"items","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.clear_options","title":"clear_options","text":"
    clear_options()\n

    Clear the content of the selection list.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect","title":"deselect","text":"
    deselect(selection)\n

    Mark the given selection as not selected.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to mark as not selected.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.deselect_all","title":"deselect_all","text":"
    deselect_all()\n

    Deselect all items.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.get_option","title":"get_option","text":"
    get_option(option_id)\n

    Get the selection option with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the selection option to get.

    required

    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.SelectionList.get_option(option_id)","title":"option_id","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.get_option_at_index","title":"get_option_at_index","text":"
    get_option_at_index(index)\n

    Get the selection option at the given index.

    Parameters:

    Name Type Description Default int

    The index of the selection option to get.

    required

    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.SelectionList.get_option_at_index(index)","title":"index","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select","title":"select","text":"
    select(selection)\n

    Mark the given selection as selected.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to mark as selected.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.select_all","title":"select_all","text":"
    select_all()\n

    Select all items.

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle","title":"toggle","text":"
    toggle(selection)\n

    Toggle the selected state of the given selection.

    Parameters:

    Name Type Description Default Selection[SelectionType] | SelectionType

    The selection to toggle.

    required

    Returns:

    Type Description Self

    The SelectionList instance.

    "},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle(selection)","title":"selection","text":""},{"location":"widgets/selection_list/#textual.widgets.SelectionList.toggle_all","title":"toggle_all","text":"
    toggle_all()\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","text":"
    Selection(\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:

    Name Type Description Default TextType

    The prompt for the selection.

    required SelectionType

    The value for the selection.

    required bool

    The initial selected state of the selection.

    False str | None

    The optional ID for the selection.

    None bool

    The initial enabled/disabled state. Enabled by default.

    False"},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(prompt)","title":"prompt","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(value)","title":"value","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(initial_state)","title":"initial_state","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(id)","title":"id","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection(disabled)","title":"disabled","text":""},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection.initial_state","title":"initial_state property","text":"
    initial_state\n

    The initial selected state for the selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection.value","title":"value property","text":"
    value\n

    The value for this selection.

    "},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError","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 color used for the smaller values in the data.

    Bases: Widget

    A sparkline widget to display numerical data.

    Parameters:

    Name Type Description Default Sequence[float] | None

    The initial data to populate the sparkline with.

    None Callable[[Sequence[float]], float] | None

    Summarizes bar values into a single value used to represent each bar.

    None str | None

    The name of the widget.

    None str | None

    The ID of the widget in the DOM.

    None str | None

    The CSS classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False"},{"location":"widgets/sparkline/#textual.widgets.Sparkline(data)","title":"data","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(summary_function)","title":"summary_function","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(name)","title":"name","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(id)","title":"id","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(classes)","title":"classes","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline(disabled)","title":"disabled","text":""},{"location":"widgets/sparkline/#textual.widgets.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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 color used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets.Sparkline.data","title":"data class-attribute instance-attribute","text":"
    data = data\n

    The data that populates the sparkline.

    "},{"location":"widgets/sparkline/#textual.widgets.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

    Bases: Widget

    A widget to display simple static content, or use as a base class for more complex widgets.

    Parameters:

    Name Type Description Default RenderableType

    A Rich renderable, or string containing console markup.

    '' bool

    Expand content if required to fill container.

    False bool

    Shrink content if required to fill container.

    False bool

    True if markup should be parsed and rendered.

    True str | None

    Name of widget.

    None str | None

    ID of Widget.

    None str | None

    Space separated list of class names.

    None bool

    Whether the static is disabled or not.

    False"},{"location":"widgets/static/#textual.widgets.Static(renderable)","title":"renderable","text":""},{"location":"widgets/static/#textual.widgets.Static(expand)","title":"expand","text":""},{"location":"widgets/static/#textual.widgets.Static(shrink)","title":"shrink","text":""},{"location":"widgets/static/#textual.widgets.Static(markup)","title":"markup","text":""},{"location":"widgets/static/#textual.widgets.Static(name)","title":"name","text":""},{"location":"widgets/static/#textual.widgets.Static(id)","title":"id","text":""},{"location":"widgets/static/#textual.widgets.Static(classes)","title":"classes","text":""},{"location":"widgets/static/#textual.widgets.Static(disabled)","title":"disabled","text":""},{"location":"widgets/static/#textual.widgets.Static.update","title":"update","text":"
    update(renderable='')\n

    Update the widget's content area with new text or Rich renderable.

    Parameters:

    Name Type Description Default RenderableType

    A new rich renderable. Defaults to empty renderable;

    ''"},{"location":"widgets/static/#textual.widgets.Static.update(renderable)","title":"renderable","text":""},{"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;.

    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:

    Name Type Description Default bool

    The initial value of the switch.

    False bool

    True if the switch should animate when toggled.

    True str | None

    The name of the switch.

    None str | None

    The ID of the switch in the DOM.

    None str | None

    The CSS classes of the switch.

    None bool

    Whether the switch is disabled or not.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/switch/#textual.widgets.Switch(value)","title":"value","text":""},{"location":"widgets/switch/#textual.widgets.Switch(animate)","title":"animate","text":""},{"location":"widgets/switch/#textual.widgets.Switch(name)","title":"name","text":""},{"location":"widgets/switch/#textual.widgets.Switch(id)","title":"id","text":""},{"location":"widgets/switch/#textual.widgets.Switch(classes)","title":"classes","text":""},{"location":"widgets/switch/#textual.widgets.Switch(disabled)","title":"disabled","text":""},{"location":"widgets/switch/#textual.widgets.Switch(tooltip)","title":"tooltip","text":""},{"location":"widgets/switch/#textual.widgets.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"enter,space\", \"toggle_switch\", \"Toggle\", show=False\n    )\n]\n
    Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {'switch--slider'}\n
    Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets.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.Changed","title":"Changed","text":"
    Changed(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.Changed.control","title":"control property","text":"
    control\n

    Alias for self.switch.

    "},{"location":"widgets/switch/#textual.widgets.Switch.action_toggle_switch","title":"action_toggle_switch","text":"
    action_toggle_switch()\n

    Toggle the state of the switch.

    "},{"location":"widgets/switch/#textual.widgets.Switch.toggle","title":"toggle","text":"
    toggle()\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 Lady\u00a0Jessica 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\u00a0Leto\u00a0\u00a0j\u00a0Jessica\u00a0\u00a0p\u00a0Paul\u00a0\u258f^p\u00a0palette

    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.Cleared
    • 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

    Bases: Widget

    A container with associated tabs to toggle content visibility.

    Parameters:

    Name Type Description Default TextType

    Positional argument will be used as title.

    () str

    The id of the initial tab, or empty string to select the first tab.

    '' str | None

    The name of the tabbed content.

    None str | None

    The ID of the tabbed content in the DOM.

    None str | None

    The CSS classes of the tabbed content.

    None bool

    Whether the tabbed content is disabled or not.

    False

    Bases: Widget

    A container for switchable content, with additional title.

    This widget is intended to be used with TabbedContent.

    Parameters:

    Name Type Description Default TextType

    Title of the TabPane (will be displayed in a tab label).

    required Widget

    Widget to go inside the TabPane.

    () str | None

    Optional name for the TabPane.

    None str | None

    Optional ID for the TabPane.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the TabPane is disabled or not.

    False"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(*titles)","title":"*titles","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(initial)","title":"initial","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(name)","title":"name","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(id)","title":"id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(classes)","title":"classes","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent(disabled)","title":"disabled","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.active","title":"active class-attribute instance-attribute","text":"
    active = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.active_pane","title":"active_pane property","text":"
    active_pane\n

    The currently active pane, or None if no pane is active.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.tab_count","title":"tab_count property","text":"
    tab_count\n

    Total number of tabs.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared","title":"Cleared","text":"
    Cleared(tabbed_content)\n

    Bases: Message

    Posted when no tab pane is active.

    This can happen if all tab panes are removed or if the currently active tab pane is unset.

    Parameters:

    Name Type Description Default TabbedContent

    The TabbedContent widget.

    required"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared(tabbed_content)","title":"tabbed_content","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.Cleared.control","title":"control property","text":"
    control\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.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.TabbedContent.TabActivated","title":"TabActivated","text":"
    TabActivated(tabbed_content, tab)\n

    Bases: Message

    Posted when the active tab changes.

    Parameters:

    Name Type Description Default TabbedContent

    The TabbedContent widget.

    required ContentTab

    The Tab widget that was selected (contains the tab label).

    required"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.TabActivated(tabbed_content)","title":"tabbed_content","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.TabActivated(tab)","title":"tab","text":""},{"location":"widgets/tabbed_content/#textual.widgets.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.TabbedContent.TabActivated.control","title":"control property","text":"
    control\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.TabbedContent.TabActivated.pane","title":"pane instance-attribute","text":"
    pane = get_pane(tab)\n

    The TabPane widget that was activated by selecting the tab.

    "},{"location":"widgets/tabbed_content/#textual.widgets.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.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.TabbedContent.add_pane","title":"add_pane","text":"
    add_pane(pane, *, before=None, after=None)\n

    Add a new pane to the tabbed content.

    Parameters:

    Name Type Description Default TabPane

    The pane to add.

    required TabPane | str | None

    Optional pane or pane ID to add the pane before.

    None TabPane | str | None

    Optional pane or pane ID to add the pane after.

    None

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object that waits for the pane to be added.

    Raises:

    Type Description 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.TabbedContent.add_pane(pane)","title":"pane","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.add_pane(before)","title":"before","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.add_pane(after)","title":"after","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.clear_panes","title":"clear_panes","text":"
    clear_panes()\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.TabbedContent.disable_tab","title":"disable_tab","text":"
    disable_tab(tab_id)\n

    Disables the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to disable.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.disable_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.enable_tab","title":"enable_tab","text":"
    enable_tab(tab_id)\n

    Enables the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to enable.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.enable_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.get_pane","title":"get_pane","text":"
    get_pane(pane_id)\n

    Get the TabPane associated with the given ID or tab.

    Parameters:

    Name Type Description Default str | ContentTab

    The ID of the pane to get, or the Tab it is associated with.

    required

    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.TabbedContent.get_pane(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.get_tab","title":"get_tab","text":"
    get_tab(pane_id)\n

    Get the Tab associated with the given ID or TabPane.

    Parameters:

    Name Type Description Default str | TabPane

    The ID of the pane, or the pane itself.

    required

    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.TabbedContent.get_tab(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.hide_tab","title":"hide_tab","text":"
    hide_tab(tab_id)\n

    Hides the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to hide.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.hide_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.remove_pane","title":"remove_pane","text":"
    remove_pane(pane_id)\n

    Remove a given pane from the tabbed content.

    Parameters:

    Name Type Description Default str

    The ID of the pane to remove.

    required

    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.TabbedContent.remove_pane(pane_id)","title":"pane_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.show_tab","title":"show_tab","text":"
    show_tab(tab_id)\n

    Shows the tab with the given ID.

    Parameters:

    Name Type Description Default str

    The ID of the TabPane to show.

    required

    Raises:

    Type Description TabError

    If there are any issues with the request.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent.show_tab(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(title)","title":"title","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(*children)","title":"*children","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(name)","title":"name","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(id)","title":"id","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(classes)","title":"classes","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane(disabled)","title":"disabled","text":""},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Disabled","title":"Disabled dataclass","text":"
    Disabled(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a tab pane is disabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Enabled","title":"Enabled dataclass","text":"
    Enabled(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a tab pane is enabled via its reactive disabled.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.Focused","title":"Focused dataclass","text":"
    Focused(tab_pane)\n

    Bases: TabPaneMessage

    Sent when a child widget is focused.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.TabPaneMessage","title":"TabPaneMessage dataclass","text":"
    TabPaneMessage(tab_pane)\n

    Bases: Message

    Base class for TabPane messages.

    "},{"location":"widgets/tabbed_content/#textual.widgets.TabPane.TabPaneMessage.control","title":"control property","text":"
    control\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.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
    tab_pane\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\u00a0Add\u00a0tab\u00a0\u00a0r\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0c\u00a0Clear\u00a0tabs\u00a0\u258f^p\u00a0palette

    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.

    Bases: Widget

    A row of tabs.

    Parameters:

    Name Type Description Default Tab | TextType

    Positional argument should be explicit Tab objects, or a str or Text.

    () str | None

    ID of the tab which should be active on start.

    None str | None

    Optional name for the tabs widget.

    None str | None

    Optional ID for the widget.

    None str | None

    Optional initial classes for the widget.

    None bool

    Whether the widget is disabled or not.

    False

    Bases: Static

    A Widget to manage a single tab within a Tabs widget.

    Parameters:

    Name Type Description Default TextType

    The label to use in the tab.

    required str | None

    Optional ID for the widget.

    None str | None

    Space separated list of class names.

    None bool

    Whether the tab is disabled or not.

    False"},{"location":"widgets/tabs/#textual.widgets.Tabs(*tabs)","title":"*tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(active)","title":"active","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(name)","title":"name","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(id)","title":"id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(classes)","title":"classes","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs(disabled)","title":"disabled","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\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.active","title":"active class-attribute instance-attribute","text":"
    active = reactive('', init=False)\n

    The ID of the active tab, or empty string if none are active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.active_tab","title":"active_tab property","text":"
    active_tab\n

    The currently active tab, or None if there are no active tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.tab_count","title":"tab_count property","text":"
    tab_count\n

    Total number of tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared","title":"Cleared","text":"
    Cleared(tabs)\n

    Bases: Message

    Sent when there are no active tabs.

    This can occur when Tabs are cleared, if all tabs are hidden, or if the currently active tab is unset.

    Parameters:

    Name Type Description Default Tabs

    The tabs widget.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.Cleared.control","title":"control property","text":"
    control\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.Cleared.tabs","title":"tabs instance-attribute","text":"
    tabs = tabs\n

    The tabs widget which was cleared.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated","title":"TabActivated","text":"
    TabActivated(tabs, tab)\n

    Bases: TabMessage

    Sent when a new tab is activated.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabActivated(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled","title":"TabDisabled","text":"
    TabDisabled(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is disabled.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabDisabled(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled","title":"TabEnabled","text":"
    TabEnabled(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is enabled.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabEnabled(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabError","title":"TabError","text":"

    Bases: Exception

    Exception raised when there is an error relating to tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden","title":"TabHidden","text":"
    TabHidden(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is hidden.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabHidden(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage","title":"TabMessage","text":"
    TabMessage(tabs, tab)\n

    Bases: Message

    Parent class for all messages that have to do with a specific tab.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.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.TabMessage.control","title":"control property","text":"
    control\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.TabMessage.tab","title":"tab instance-attribute","text":"
    tab = tab\n

    The tab that is the object of this message.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
    tabs = tabs\n

    The tabs widget containing the tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown","title":"TabShown","text":"
    TabShown(tabs, tab)\n

    Bases: TabMessage

    Sent when a tab is shown.

    Parameters:

    Name Type Description Default Tabs

    The Tabs widget.

    required Tab

    The tab that is the object of this message.

    required"},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown(tabs)","title":"tabs","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.TabShown(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.action_next_tab","title":"action_next_tab","text":"
    action_next_tab()\n

    Make the next tab active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.action_previous_tab","title":"action_previous_tab","text":"
    action_previous_tab()\n

    Make the previous tab active.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab","title":"add_tab","text":"
    add_tab(tab, *, before=None, after=None)\n

    Add a new tab to the end of the tab list.

    Parameters:

    Name Type Description Default Tab | str | Text

    A new tab object, or a label (str or Text).

    required Tab | str | None

    Optional tab or tab ID to add the tab before.

    None Tab | str | None

    Optional tab or tab ID to add the tab after.

    None

    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 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.add_tab(tab)","title":"tab","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab(before)","title":"before","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.add_tab(after)","title":"after","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.clear","title":"clear","text":"
    clear()\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.disable","title":"disable","text":"
    disable(tab_id)\n

    Disable the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to disable.

    required

    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.disable(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.enable","title":"enable","text":"
    enable(tab_id)\n

    Enable the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to enable.

    required

    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.enable(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.hide","title":"hide","text":"
    hide(tab_id)\n

    Hide the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to hide.

    required

    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.hide(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.remove_tab","title":"remove_tab","text":"
    remove_tab(tab_or_id)\n

    Remove a tab.

    Parameters:

    Name Type Description Default Tab | str | None

    The Tab to remove or its id.

    required

    Returns:

    Type Description AwaitComplete

    An optionally awaitable object that waits for the tab to be removed.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.remove_tab(tab_or_id)","title":"tab_or_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.show","title":"show","text":"
    show(tab_id)\n

    Show the indicated tab.

    Parameters:

    Name Type Description Default str

    The ID of the Tab to show.

    required

    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.show(tab_id)","title":"tab_id","text":""},{"location":"widgets/tabs/#textual.widgets.Tabs.validate_active","title":"validate_active","text":"
    validate_active(active)\n

    Check id assigned to active attribute is a valid tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tabs.watch_active","title":"watch_active","text":"
    watch_active(previously_active, active)\n

    Handle a change to the active tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab(label)","title":"label","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(id)","title":"id","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(classes)","title":"classes","text":""},{"location":"widgets/tabs/#textual.widgets.Tab(disabled)","title":"disabled","text":""},{"location":"widgets/tabs/#textual.widgets.Tab.label","title":"label property writable","text":"
    label\n

    The label for the tab.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.label_text","title":"label_text property","text":"
    label_text\n

    Undecorated text of the label.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Clicked","title":"Clicked dataclass","text":"
    Clicked(tab)\n

    Bases: TabMessage

    A tab was clicked.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Disabled","title":"Disabled dataclass","text":"
    Disabled(tab)\n

    Bases: TabMessage

    A tab was disabled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Enabled","title":"Enabled dataclass","text":"
    Enabled(tab)\n

    Bases: TabMessage

    A tab was enabled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.Relabelled","title":"Relabelled dataclass","text":"
    Relabelled(tab)\n

    Bases: TabMessage

    A tab was relabelled.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.TabMessage","title":"TabMessage dataclass","text":"
    TabMessage(tab)\n

    Bases: Message

    Tab-related messages.

    These are mostly intended for internal use when interacting with Tabs.

    "},{"location":"widgets/tabs/#textual.widgets.Tab.TabMessage.control","title":"control property","text":"
    control\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.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
    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 \u258a1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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

    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.code_editor(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.

    Tip

    The TextArea.document.end property returns the location at the end of the document, which might be convenient when editing programmatically.

    "},{"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 \u258a1\u00a0\u00a0defhello(name):\u258e \u258a2\u00a0\u00a0print(\"hello\"+\u00a0name)\u258e \u258a3\u00a0\u00a0\u258e \u258a4\u00a0\u00a0defgoodbye(name):\u00a0\u258e \u258a5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0\u258e \u258a6\u00a0\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

    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.code_editor(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":"

    Many 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, which takes its 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.

    When using the css theme, you can make use of component classes to style elements of the TextArea. For example, the CSS code TextArea .text-area--cursor { background: green; } will make the cursor green.

    More complex applications such as code editors may 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.code_editor(\"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{'css', '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":"

    Note

    Custom themes are only relevant for people who are looking to customize syntax highlighting. If you're only editing plain text, and wish to recolor aspects of the TextArea, you should use the provided component classes.

    Using custom (non-builtin) themes is a 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-and-escape-behavior","title":"Tab and Escape behavior","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_behavior attribute to the string value \"indent\". While in this mode, you can shift focus by pressing the Esc key.

    "},{"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/#undo-and-redo","title":"Undo and redo","text":"

    TextArea offers undo and redo methods. By default, undo is bound to Ctrl+Z and redo to Ctrl+Y.

    The TextArea uses a heuristic to place checkpoints after certain types of edit. When you call undo, all of the edits between now and the most recent checkpoint are reverted. You can manually add a checkpoint by calling the TextArea.history.checkpoint() instance method.

    The undo and redo history uses a stack-based system, where a single item on the stack represents a single checkpoint. In memory-constrained environments, you may wish to reduce the maximum number of checkpoints that can exist. You can do this by passing the max_checkpoints argument to the TextArea constructor.

    "},{"location":"widgets/text_area/#read-only-mode","title":"Read-only mode","text":"

    TextArea.read_only is a boolean reactive attribute which, if True, will prevent users from modifying content in the TextArea.

    While read_only=True, you can still modify the content programmatically.

    While this mode is active, the TextArea receives the -read-only CSS class, which you can use to supply custom styles for read-only mode.

    "},{"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.

    You can also change the start line number (the topmost line number in the gutter) by setting the line_number_start reactive attribute.

    "},{"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.code_editor(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 \u258a1\u00a0\u00a0def\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.code_editor(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 \u258a1\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\u258e \u258a2\u00a0\u00a0publicstatic\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0\u258e \u258a3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0\u258e \u258a4\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\u00a0\u258e \u258a5\u00a0\u00a0}\u00a0\u258e \u258a6\u00a0\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\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 key-value pair 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 \"css\" The theme to use. selection Selection Selection() The current selection. show_line_numbers bool False Show or hide line numbers. line_number_start int 1 The start line number in the gutter. 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. read_only bool False Enable/disable read-only mode."},{"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 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. ctrl+z Undo. ctrl+y Redo."},{"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
    • EditHistory - manages the undo stack
    • 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.

    Bases: ScrollView

    Parameters:

    Name Type Description Default str

    The initial text to load into the TextArea.

    '' str | None

    The language to use.

    None str

    The theme to use.

    'css' bool

    Enable soft wrapping.

    True Literal['focus', 'indent']

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    'focus' bool

    Enable read-only mode. This prevents edits using the keyboard.

    False bool

    Show line numbers on the left edge.

    False int

    What line number to start on.

    1 int

    The maximum number of undo history checkpoints to retain.

    50 str | None

    The name of the TextArea widget.

    None str | None

    The ID of the widget, used to refer to it from Textual CSS.

    None str | None

    One or more Textual CSS compatible class names separated by spaces.

    None bool

    True if the widget is disabled.

    False RenderableType | None

    Optional tooltip.

    None"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(theme)","title":"theme","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(soft_wrap)","title":"soft_wrap","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(tab_behavior)","title":"tab_behavior","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(read_only)","title":"read_only","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(show_line_numbers)","title":"show_line_numbers","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(line_number_start)","title":"line_number_start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(max_checkpoints)","title":"max_checkpoints","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(id)","title":"id","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(classes)","title":"classes","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(disabled)","title":"disabled","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea(tooltip)","title":"tooltip","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
    BINDINGS = [\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 character 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 character 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_or_delete_line\",\n        \"Delete to line end\",\n        show=False,\n    ),\n    Binding(\"ctrl+z\", \"undo\", \"Undo\", show=False),\n    Binding(\"ctrl+y\", \"redo\", \"Redo\", show=False),\n]\n
    Key(s) Description 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. ctrl+z Undo. ctrl+y Redo."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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\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\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\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\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\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\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\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\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(True, init=False)\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\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\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 = 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\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.history","title":"history instance-attribute","text":"
    history = EditHistory(\n    max_checkpoints=max_checkpoints,\n    checkpoint_timer=2.0,\n    checkpoint_max_characters=100,\n)\n

    A stack (the end of the list is the top of the stack) for tracking edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
    indent_type = '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(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\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 = 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.line_number_start","title":"line_number_start class-attribute instance-attribute","text":"
    line_number_start = reactive(1, init=False)\n

    The line number the first line should be.

    "},{"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(True, init=False)\n

    If the cursor is at a bracket, highlight the matching bracket (if found).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.matching_bracket_location","title":"matching_bracket_location property","text":"
    matching_bracket_location\n

    The location of the matching bracket, if there is one.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.navigator","title":"navigator instance-attribute","text":"
    navigator = DocumentNavigator(wrapped_document)\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.read_only","title":"read_only class-attribute instance-attribute","text":"
    read_only = reactive(False)\n

    True if the content is read-only.

    Read-only means end users cannot insert, delete or replace content.

    The document can still be edited programmatically via the API.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
    selected_text\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(\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(False, init=False)\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(True, init=False)\n

    True if text should soft wrap.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text property writable","text":"
    text\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 = 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\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(document)\n

    The wrapped view of the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.Changed","title":"Changed dataclass","text":"
    Changed(text_area)\n

    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\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\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged","title":"SelectionChanged dataclass","text":"
    SelectionChanged(selection, text_area)\n

    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\n

    The new selection.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.SelectionChanged.text_area","title":"text_area instance-attribute","text":"
    text_area\n

    The text_area that sent this message.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down(select=False)\n

    Move the cursor down one cell.

    Parameters:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left","text":"
    action_cursor_left(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:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end","text":"
    action_cursor_line_end(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","text":"
    action_cursor_line_start(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","text":"
    action_cursor_page_down()\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","text":"
    action_cursor_page_up()\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","text":"
    action_cursor_right(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:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up(select=False)\n

    Move the cursor up one cell.

    Parameters:

    Name Type Description Default bool

    If True, select the text while moving.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left","text":"
    action_cursor_word_left(select=False)\n

    Move the cursor left by a single word, skipping trailing whitespace.

    Parameters:

    Name Type Description Default bool

    Whether to select while moving the cursor.

    False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right","text":"
    action_cursor_word_right(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","text":"
    action_delete_left()\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","text":"
    action_delete_line()\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","text":"
    action_delete_right()\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","text":"
    action_delete_to_end_of_line()\n

    Deletes from the cursor location to the end of the line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line_or_delete_line","title":"action_delete_to_end_of_line_or_delete_line async","text":"
    action_delete_to_end_of_line_or_delete_line()\n

    Deletes from the cursor location to the end of the line, or deletes the line.

    The line will be deleted if the line is empty.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line","text":"
    action_delete_to_start_of_line()\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","text":"
    action_delete_word_left()\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","text":"
    action_delete_word_right()\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_redo","title":"action_redo","text":"
    action_redo()\n

    Redo the most recently undone batch of edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all","text":"
    action_select_all()\n

    Select all the text in the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line","text":"
    action_select_line()\n

    Select all the text on the current line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_undo","title":"action_undo","text":"
    action_undo()\n

    Undo the edits since the last checkpoint (the most recent batch of edits).

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index","text":"
    cell_width_to_column_index(cell_width, row_index)\n

    Return the column that the cell width corresponds to on the given row.

    Parameters:

    Name Type Description Default int

    The cell width to convert.

    required int

    The index of the row to examine.

    required

    Returns:

    Type Description int

    The column corresponding to the cell width on that row.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index(cell_width)","title":"cell_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index(row_index)","title":"row_index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key","title":"check_consume_key","text":"
    check_consume_key(key, character=None)\n

    Check if the widget may consume the given key.

    As a textarea we are expecting to capture printable keys.

    Parameters:

    Name Type Description Default str

    A key identifier.

    required str | None

    A character associated with the key, or None if there isn't one.

    None

    Returns:

    Type Description bool

    True if the widget may capture the key in it's Key message, or False if it won't.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key(key)","title":"key","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.check_consume_key(character)","title":"character","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable","text":"
    clamp_visitable(location)\n

    Clamp the given location to the nearest visitable location.

    Parameters:

    Name Type Description Default Location

    The location to clamp.

    required

    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.clamp_visitable(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear","text":"
    clear()\n

    Delete all text from the document.

    Returns:

    Type Description EditResult

    An EditResult relating to the deletion of all content.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor","title":"code_editor classmethod","text":"
    code_editor(\n    text=\"\",\n    *,\n    language=None,\n    theme=\"monokai\",\n    soft_wrap=False,\n    tab_behavior=\"indent\",\n    read_only=False,\n    show_line_numbers=True,\n    line_number_start=1,\n    max_checkpoints=50,\n    name=None,\n    id=None,\n    classes=None,\n    disabled=False,\n    tooltip=None\n)\n

    Construct a new TextArea with sensible defaults for editing code.

    This instantiates a TextArea with line numbers enabled, soft wrapping disabled, \"indent\" tab behavior, and the \"monokai\" theme.

    Parameters:

    Name Type Description Default str

    The initial text to load into the TextArea.

    '' str | None

    The language to use.

    None str

    The theme to use.

    'monokai' bool

    Enable soft wrapping.

    False Literal['focus', 'indent']

    If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.

    'indent' bool

    Show line numbers on the left edge.

    True int

    What line number to start on.

    1 str | None

    The name of the TextArea widget.

    None str | None

    The ID of the widget, used to refer to it from Textual CSS.

    None str | None

    One or more Textual CSS compatible class names separated by spaces.

    None bool

    True if the widget is disabled.

    False RenderableType | None

    Optional tooltip

    None"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(theme)","title":"theme","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(soft_wrap)","title":"soft_wrap","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(tab_behavior)","title":"tab_behavior","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(show_line_numbers)","title":"show_line_numbers","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(line_number_start)","title":"line_number_start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(id)","title":"id","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(classes)","title":"classes","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(disabled)","title":"disabled","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.code_editor(tooltip)","title":"tooltip","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete","text":"
    delete(start, end, *, maintain_selection_offset=True)\n

    Delete the text between two locations in the document.

    Parameters:

    Name Type Description Default Location

    The start location.

    required Location

    The end location.

    required bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit","text":"
    edit(edit)\n

    Perform an Edit.

    Parameters:

    Name Type Description Default Edit

    The Edit to perform.

    required

    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.edit(edit)","title":"edit","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket","text":"
    find_matching_bracket(bracket, search_from)\n

    If the character is a bracket, find the matching bracket.

    Parameters:

    Name Type Description Default str

    The character we're searching for the matching bracket of.

    required Location

    The location to start the search.

    required

    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.find_matching_bracket(bracket)","title":"bracket","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket(search_from)","title":"search_from","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width","text":"
    get_column_width(row, column)\n

    Get the cell offset of the column from the start of the row.

    Parameters:

    Name Type Description Default int

    The row index.

    required int

    The column index (codepoint offset from start of row).

    required

    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_column_width(row)","title":"row","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width(column)","title":"column","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location","text":"
    get_cursor_down_location()\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","text":"
    get_cursor_left_location()\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","text":"
    get_cursor_line_end_location()\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","text":"
    get_cursor_line_start_location(smart_home=False)\n

    Get the location of the start of the current line.

    Parameters:

    Name Type Description Default bool

    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.

    False

    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_line_start_location(smart_home)","title":"smart_home","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location","text":"
    get_cursor_right_location()\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","text":"
    get_cursor_up_location()\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","text":"
    get_cursor_word_left_location()\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","text":"
    get_cursor_word_right_location()\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_line","title":"get_line","text":"
    get_line(line_index)\n

    Retrieve the line at the given line index.

    You can stylize the Text object returned here to apply additional styling to TextArea content.

    Parameters:

    Name Type Description Default int

    The index of the line.

    required

    Returns:

    Type Description Text

    A rich.Text object containing the requested line.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_line(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location","text":"
    get_target_document_location(event)\n

    Given a MouseEvent, return the row and column offset of the event in document-space.

    Parameters:

    Name Type Description Default MouseEvent

    The MouseEvent.

    required

    Returns:

    Type Description Location

    The location of the mouse event within the document.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location(event)","title":"event","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range","text":"
    get_text_range(start, end)\n

    Get the text between a start and end location.

    Parameters:

    Name Type Description Default Location

    The start location.

    required Location

    The end location.

    required

    Returns:

    Type Description str

    The text between start and end.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert","text":"
    insert(\n    text, location=None, *, maintain_selection_offset=True\n)\n

    Insert text into the document.

    Parameters:

    Name Type Description Default str

    The text to insert.

    required Location | None

    The location to insert text, or None to use the cursor location.

    None bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text","text":"
    load_text(text)\n

    Load text into the TextArea.

    This will replace the text currently in the TextArea and clear the edit history.

    Parameters:

    Name Type Description Default str

    The text to load into the TextArea.

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor","text":"
    move_cursor(\n    location, select=False, center=False, record_width=True\n)\n

    Move the cursor to a location.

    Parameters:

    Name Type Description Default Location

    The location to move the cursor to.

    required bool

    If True, select text between the old and new location.

    False bool

    If True, scroll such that the cursor is centered.

    False bool

    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.

    True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor(record_width)","title":"record_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative","text":"
    move_cursor_relative(\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:

    Name Type Description Default int

    The number of rows to move down by (negative to move up)

    0 int

    The number of columns to move right by (negative to move left)

    0 bool

    If True, select text between the old and new location.

    False bool

    If True, scroll such that the cursor is centered.

    False bool

    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.

    True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(rows)","title":"rows","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(columns)","title":"columns","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(select)","title":"select","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative(record_width)","title":"record_width","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width","text":"
    record_cursor_width()\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.redo","title":"redo","text":"
    redo()\n

    Redo the most recently undone batch of edits.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language","text":"
    register_language(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:

    Name Type Description Default 'str | Language'

    A string referring to a builtin language or a tree-sitter Language object.

    required str

    The highlight query to use for syntax highlighting this language.

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language(highlight_query)","title":"highlight_query","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme","text":"
    register_theme(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","text":"
    replace(\n    insert, start, end, *, maintain_selection_offset=True\n)\n

    Replace text in the document with new text.

    Parameters:

    Name Type Description Default str

    The text to insert.

    required Location

    The start location

    required Location

    The end location.

    required bool

    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.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the edit.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(insert)","title":"insert","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace(maintain_selection_offset)","title":"maintain_selection_offset","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible","text":"
    scroll_cursor_visible(center=False, animate=False)\n

    Scroll the TextArea such that the cursor is visible on screen.

    Parameters:

    Name Type Description Default bool

    True if the cursor should be scrolled to the center.

    False bool

    True if we should animate while scrolling.

    False

    Returns:

    Type Description Offset

    The offset that was scrolled to bring the cursor into view.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible(center)","title":"center","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible(animate)","title":"animate","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all","text":"
    select_all()\n

    Select all of the text in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line","text":"
    select_line(index)\n

    Select all the text in the specified line.

    Parameters:

    Name Type Description Default int

    The index of the line to select (starting from 0).

    required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.undo","title":"undo","text":"
    undo()\n

    Undo the edits since the last checkpoint (the most recent batch of edits).

    "},{"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","text":"
    Document(text)\n

    Bases: DocumentBase

    A document which can be opened in a TextArea.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.end","title":"end property","text":"
    end\n

    Returns the location of the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.line_count","title":"line_count property","text":"
    line_count\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.lines","title":"lines property","text":"
    lines\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.widgets.text_area.Document.newline","title":"newline property","text":"
    newline\n

    Get the Newline used in this document (e.g. ' ', ' '. etc.)

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.start","title":"start property","text":"
    start\n

    Returns the location of the start of the document (0, 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.text","title":"text property","text":"
    text\n

    Get the text from the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_index_from_location","title":"get_index_from_location","text":"
    get_index_from_location(location)\n

    Given a location, returns the index from the document's text.

    Parameters:

    Name Type Description Default Location

    The location in the document.

    required

    Returns:

    Type Description int

    The index in the document's text.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_index_from_location(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_line","title":"get_line","text":"
    get_line(index)\n

    Returns the line with the given index from the document.

    Parameters:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_location_from_index","title":"get_location_from_index","text":"
    get_location_from_index(index)\n

    Given an index in the document's text, returns the corresponding location.

    Parameters:

    Name Type Description Default int

    The index in the document's text.

    required

    Returns:

    Type Description Location

    The corresponding location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_location_from_index(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_size","title":"get_size","text":"
    get_size(tab_width)\n

    The Size of the document, taking into account the tab rendering width.

    Parameters:

    Name Type Description Default int

    The width to use for tab indents.

    required

    Returns:

    Type Description Size

    The size (width, height) of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_size(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range","title":"get_text_range","text":"
    get_text_range(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:

    Name Type Description Default Location

    The start location of the selection.

    required Location

    The end location of the selection.

    required

    Returns:

    Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range","title":"replace_range","text":"
    replace_range(start, end, text)\n

    Replace text at the given range.

    This is the only method by which a document may be updated.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The EditResult containing information about the completed replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Document.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase","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.widgets.text_area.DocumentBase.end","title":"end abstractmethod property","text":"
    end\n

    Returns the location of the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.line_count","title":"line_count abstractmethod property","text":"
    line_count\n

    Returns the number of lines in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.lines","title":"lines abstractmethod property","text":"
    lines\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.widgets.text_area.DocumentBase.newline","title":"newline abstractmethod property","text":"
    newline\n

    Return the line separator used in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.start","title":"start abstractmethod property","text":"
    start\n

    Returns the location of the start of the document (0, 0).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.text","title":"text abstractmethod property","text":"
    text\n

    The text from the document as a string.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_line","title":"get_line abstractmethod","text":"
    get_line(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:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description str

    The str instance representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_line(index)","title":"index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_size","title":"get_size abstractmethod","text":"
    get_size(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:

    Name Type Description Default int

    The width to use for tab characters.

    required

    Returns:

    Type Description Size

    The Size of the document bounding box.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_size(indent_width)","title":"indent_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
    get_text_range(start, end)\n

    Get the text that falls between the start and end locations.

    Parameters:

    Name Type Description Default Location

    The start location of the selection.

    required Location

    The end location of the selection.

    required

    Returns:

    Type Description str

    The text between start (inclusive) and end (exclusive).

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.get_text_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree","title":"query_syntax_tree","text":"
    query_syntax_tree(query, start_point=None, end_point=None)\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:

    Name Type Description Default Query

    The tree-sitter Query to perform.

    required tuple[int, int] | None

    The (row, column byte) to start the query at.

    None tuple[int, int] | None

    The (row, column byte) to end the query at.

    None

    Returns:

    Type Description list[tuple[Node, str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(start_point)","title":"start_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.query_syntax_tree(end_point)","title":"end_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
    replace_range(start, end, text)\n

    Replace the text at the given range.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator","title":"DocumentNavigator","text":"
    DocumentNavigator(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 behavior.

    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 behavior. 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 behavior applies. This is designed to make cursor movement more useful to end users.

    Parameters:

    Name Type Description Default WrappedDocument

    The WrappedDocument to be used when making navigation decisions.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator(wrapped_document)","title":"wrapped_document","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.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.widgets.text_area.DocumentNavigator.clamp_reachable","title":"clamp_reachable","text":"
    clamp_reachable(location)\n

    Given a location, return the nearest location that corresponds to a reachable location in the document.

    Parameters:

    Name Type Description Default Location

    A location.

    required

    Returns:

    Type Description Location

    The nearest reachable location in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.clamp_reachable(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_above","title":"get_location_above","text":"
    get_location_above(location)\n

    Get the location visually aligned with the cell above the given location.

    Parameters:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The cell above the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_above(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset","title":"get_location_at_y_offset","text":"
    get_location_at_y_offset(location, vertical_offset)\n

    Apply a visual vertical offset to a location and check the resulting location.

    Parameters:

    Name Type Description Default Location

    The location to start from.

    required int

    The vertical offset to move (negative=up, positive=down).

    required

    Returns:

    Type Description Location

    The location after the offset has been applied.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_at_y_offset(vertical_offset)","title":"vertical_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_below","title":"get_location_below","text":"
    get_location_below(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:

    Name Type Description Default Location

    The location in the raw document.

    required

    Returns:

    Type Description Location

    The location which is visually below the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_below(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_end","title":"get_location_end","text":"
    get_location_end(location)\n

    Get the location corresponding to the end of the current section.

    Parameters:

    Name Type Description Default Location

    The current location.

    required

    Returns:

    Type Description Location

    The location corresponding to the end of the wrapped line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_end(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home","title":"get_location_home","text":"
    get_location_home(location, smart_home=False)\n

    Get the \"home location\" corresponding to the given location.

    Parameters:

    Name Type Description Default Location

    The location to consider.

    required bool

    Enable/disable 'smart home' behavior.

    False

    Returns:

    Type Description Location

    The home location, relative to the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_home(smart_home)","title":"smart_home","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_left","title":"get_location_left","text":"
    get_location_left(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:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_left(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_right","title":"get_location_right","text":"
    get_location_right(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:

    Name Type Description Default Location

    The location to start from.

    required

    Returns:

    Type Description Location

    The location to the right.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.get_location_right(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document","title":"is_end_of_document","text":"
    is_end_of_document(location)\n

    Check if a location is at the end of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is at the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document_line","title":"is_end_of_document_line","text":"
    is_end_of_document_line(location)\n

    True if the location is at the end of a line in the document.

    Note that the \"end\" of a line is equal to its length (one greater than the final index), since there is a space at the end of the line for the cursor to rest.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the document is at the end of a line in the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_end_of_wrapped_line","title":"is_end_of_wrapped_line","text":"
    is_end_of_wrapped_line(location)\n

    True if the location is at the end of a wrapped line.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_end_of_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_document_line","title":"is_first_document_line","text":"
    is_first_document_line(location)\n

    Check if the given location is on the first line in the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is on the first line of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_first_wrapped_line","title":"is_first_wrapped_line","text":"
    is_first_wrapped_line(location)\n

    Check if the given location is on the first wrapped section of the first line in the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_first_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_document_line","title":"is_last_document_line","text":"
    is_last_document_line(location)\n

    Check if the given location is on the last line of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True when the location is on the last line of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_last_wrapped_line","title":"is_last_wrapped_line","text":"
    is_last_wrapped_line(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:

    Name Type Description Default Location

    The location to examine.

    required

    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.widgets.text_area.DocumentNavigator.is_last_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document","title":"is_start_of_document","text":"
    is_start_of_document(location)\n

    Check if a location is at the start of the document.

    Parameters:

    Name Type Description Default Location

    The location to examine.

    required

    Returns:

    Type Description bool

    True if and only if the cursor is at document location (0, 0)

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document_line","title":"is_start_of_document_line","text":"
    is_start_of_document_line(location)\n

    True when the location is at the start of the first document line.

    Parameters:

    Name Type Description Default Location

    The location to check.

    required

    Returns:

    Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_document_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_wrapped_line","title":"is_start_of_wrapped_line","text":"
    is_start_of_wrapped_line(location)\n

    True when the location is at the start of the first wrapped line.

    Parameters:

    Name Type Description Default Location

    The location to check.

    required

    Returns:

    Type Description bool

    True if the location is at column index 0.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentNavigator.is_start_of_wrapped_line(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit dataclass","text":"
    Edit(\n    text,\n    from_location,\n    to_location,\n    maintain_selection_offset,\n)\n

    Implements the Undoable protocol to replace text at some range within a document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.bottom","title":"bottom property","text":"
    bottom\n

    The Location impacted by this edit that is nearest the end of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.from_location","title":"from_location instance-attribute","text":"
    from_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\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\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\n

    The end location of the insert

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.top","title":"top property","text":"
    top\n

    The Location impacted by this edit that is nearest the start of the document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.after","title":"after","text":"
    after(text_area)\n

    Hook for running code after an Edit has been performed via Edit.do and side effects such as re-wrapping the document and refreshing the display have completed.

    For example, we can't record cursor visual offset until we know where the cursor will land after wrapping has been performed, so we must wait until here to do it.

    Parameters:

    Name Type Description Default TextArea

    The TextArea this operation was performed on.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.after(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do","title":"do","text":"
    do(text_area, record_selection=True)\n

    Perform the edit operation.

    Parameters:

    Name Type Description Default TextArea

    The TextArea to perform the edit on.

    required bool

    If True, record the current selection in the TextArea so that it may be restored if this Edit is undone in the future.

    True

    Returns:

    Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.do(record_selection)","title":"record_selection","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.undo","title":"undo","text":"
    undo(text_area)\n

    Undo the edit operation.

    Looks at the data stored in the edit, and performs the inverse operation of Edit.do.

    Parameters:

    Name Type Description Default TextArea

    The TextArea to undo the insert operation on.

    required

    Returns:

    Type Description EditResult

    An EditResult containing information about the replace operation.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Edit.undo(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory","title":"EditHistory dataclass","text":"
    EditHistory(\n    max_checkpoints,\n    checkpoint_timer,\n    checkpoint_max_characters,\n)\n

    Manages batching/checkpointing of Edits into groups that can be undone/redone in the TextArea.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint_max_characters","title":"checkpoint_max_characters instance-attribute","text":"
    checkpoint_max_characters\n

    Maximum number of characters that can appear in a batch before a new batch is formed.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint_timer","title":"checkpoint_timer instance-attribute","text":"
    checkpoint_timer\n

    Maximum number of seconds since last edit until a new batch is created.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.redo_stack","title":"redo_stack property","text":"
    redo_stack\n

    A copy of the redo stack, with references to the original Edits.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.undo_stack","title":"undo_stack property","text":"
    undo_stack\n

    A copy of the undo stack, with references to the original Edits.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.checkpoint","title":"checkpoint","text":"
    checkpoint()\n

    Ensure the next recorded edit starts a new batch.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.clear","title":"clear","text":"
    clear()\n

    Completely clear the history.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.record","title":"record","text":"
    record(edit)\n

    Record an Edit so that it may be undone and redone.

    Determines whether to batch the Edit with previous Edits, or create a new batch/checkpoint.

    This method must be called exactly once per edit, in chronological order.

    A new batch/checkpoint is created when:

    • The undo stack is empty.
    • The checkpoint timer expires.
    • The maximum number of characters permitted in a checkpoint is reached.
    • A redo is performed (we should not add new edits to a batch that has been redone).
    • The programmer has requested a new batch via a call to force_new_batch.
      • e.g. the TextArea widget may call this method in some circumstances.
      • Clicking to move the cursor elsewhere in the document should create a new batch.
      • Movement of the cursor via a keyboard action that is NOT an edit.
      • Blurring the TextArea creates a new checkpoint.
    • The current edit involves a deletion/replacement and the previous edit did not.
    • The current edit is a pure insertion and the previous edit was not.
    • The edit involves insertion or deletion of one or more newline characters.
    • An edit which inserts more than a single character (a paste) gets an isolated batch.

    Parameters:

    Name Type Description Default Edit

    The edit to record.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.EditHistory.record(edit)","title":"edit","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult dataclass","text":"
    EditResult(end_location, replaced_text)\n

    Contains information about an edit that has occurred.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult.end_location","title":"end_location instance-attribute","text":"
    end_location\n

    The new end Location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
    replaced_text\n

    The text that was replaced.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist","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","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.widgets.text_area.Selection.end","title":"end class-attribute instance-attribute","text":"
    end = (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.widgets.text_area.Selection.is_empty","title":"is_empty property","text":"
    is_empty\n

    Return True if the selection has 0 width, i.e. it's just a cursor.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.Selection.start","title":"start class-attribute instance-attribute","text":"
    start = (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.widgets.text_area.Selection.cursor","title":"cursor classmethod","text":"
    cursor(location)\n

    Create a Selection with the same start and end point - a \"cursor\".

    Parameters:

    Name Type Description Default Location

    The location to create the zero-width Selection.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.Selection.cursor(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument","text":"
    SyntaxAwareDocument(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:

    Name Type Description Default str

    The initial text contained in the document.

    required str | Language

    The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument(language)","title":"language","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
    language = get_language(language)\n

    The tree-sitter Language or None if tree-sitter is unavailable.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.get_line","title":"get_line","text":"
    get_line(line_index)\n

    Return the string representing the line, not including new line characters.

    Parameters:

    Name Type Description Default int

    The index of the line.

    required

    Returns:

    Type Description str

    The string representing the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.get_line(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.prepare_query","title":"prepare_query","text":"
    prepare_query(query)\n

    Prepare a tree-sitter tree query.

    Queries should be prepared once, then reused.

    To execute a query, call query_syntax_tree.

    Parameters:

    Name Type Description Default str

    The string query to prepare.

    required

    Returns:

    Type Description Query | None

    The prepared query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.prepare_query(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree","text":"
    query_syntax_tree(query, start_point=None, end_point=None)\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:

    Name Type Description Default Query

    The tree-sitter Query to perform.

    required tuple[int, int] | None

    The (row, column byte) to start the query at.

    None tuple[int, int] | None

    The (row, column byte) to end the query at.

    None

    Returns:

    Type Description list[tuple['Node', str]]

    A tuple containing the nodes and text captured by the query.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(query)","title":"query","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(start_point)","title":"start_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.query_syntax_tree(end_point)","title":"end_point","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range","title":"replace_range","text":"
    replace_range(start, end, text)\n

    Replace text at the given range.

    Parameters:

    Name Type Description Default Location

    A tuple (row, column) where the edit starts.

    required Location

    A tuple (row, column) where the edit ends.

    required str

    The text to insert between start and end.

    required

    Returns:

    Type Description EditResult

    The new end location after the edit is complete.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(end)","title":"end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument.replace_range(text)","title":"text","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme dataclass","text":"
    TextAreaTheme(\n    name,\n    base_style=None,\n    gutter_style=None,\n    cursor_style=None,\n    cursor_line_style=None,\n    cursor_line_gutter_style=None,\n    bracket_matching_style=None,\n    selection_style=None,\n    syntax_styles=dict(),\n)\n

    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.widgets.text_area.TextAreaTheme.base_style","title":"base_style class-attribute instance-attribute","text":"
    base_style = None\n

    The background style of the text area. If None the parent style will be used.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style class-attribute instance-attribute","text":"
    bracket_matching_style = None\n

    The style to apply to matching brackets. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style class-attribute instance-attribute","text":"
    cursor_line_gutter_style = 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.widgets.text_area.TextAreaTheme.cursor_line_style","title":"cursor_line_style class-attribute instance-attribute","text":"
    cursor_line_style = None\n

    The style to apply to the line the cursor is on.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.cursor_style","title":"cursor_style class-attribute instance-attribute","text":"
    cursor_style = None\n

    The style of the cursor. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.gutter_style","title":"gutter_style class-attribute instance-attribute","text":"
    gutter_style = None\n

    The style of the gutter. If None, a legible Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.name","title":"name instance-attribute","text":"
    name\n

    The name of the theme.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.selection_style","title":"selection_style class-attribute instance-attribute","text":"
    selection_style = None\n

    The style of the selection. If None a default selection Style will be generated.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.syntax_styles","title":"syntax_styles class-attribute instance-attribute","text":"
    syntax_styles = field(default_factory=dict)\n

    The mapping of tree-sitter names from the highlight_query to Rich styles.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.apply_css","title":"apply_css","text":"
    apply_css(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:

    Name Type Description Default TextArea

    The TextArea instance to retrieve fallback styling from.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.apply_css(text_area)","title":"text_area","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
    builtin_themes()\n

    Get a list of all builtin TextAreaThemes.

    Returns:

    Type Description list[TextAreaTheme]

    A list of all builtin TextAreaThemes.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
    get_builtin_theme(theme_name)\n

    Get a TextAreaTheme by name.

    Given a theme_name, return the corresponding TextAreaTheme object.

    Parameters:

    Name Type Description Default str

    The name of the theme.

    required

    Returns:

    Type Description TextAreaTheme | None

    The TextAreaTheme corresponding to the name or None if the theme isn't found.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_builtin_theme(theme_name)","title":"theme_name","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme.get_highlight","title":"get_highlight","text":"
    get_highlight(name)\n

    Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

    Parameters:

    Name Type Description Default str

    The name of the highlight.

    required

    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.TextAreaTheme.get_highlight(name)","title":"name","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist","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","text":"
    WrappedDocument(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:

    Name Type Description Default DocumentBase

    The document to wrap.

    required int

    The width to wrap at.

    0 int

    The maximum width to consider for tab characters.

    4"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(document)","title":"document","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(width)","title":"width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.document","title":"document instance-attribute","text":"
    document = document\n

    The document wrapping is performed on.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.height","title":"height property","text":"
    height\n

    The height of the wrapped document.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.lines","title":"lines property","text":"
    lines\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.widgets.text_area.WrappedDocument.wrapped","title":"wrapped property","text":"
    wrapped\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.widgets.text_area.WrappedDocument.get_offsets","title":"get_offsets","text":"
    get_offsets(line_index)\n

    Given a line index, get the offsets within that line where wrapping should occur for the current document.

    Parameters:

    Name Type Description Default int

    The index of the line within the document.

    required

    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.widgets.text_area.WrappedDocument.get_offsets(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_sections","title":"get_sections","text":"
    get_sections(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:

    Name Type Description Default int

    The index of the line to get sections for.

    required

    Returns:

    Type Description list[str]

    The wrapped line as a list of strings.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_sections(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_tab_widths","title":"get_tab_widths","text":"
    get_tab_widths(line_index)\n

    Return a list of the tab widths for the given line index.

    Parameters:

    Name Type Description Default int

    The index of the line in the document.

    required

    Returns:

    Type Description list[int]

    An ordered list of the expanded width of the tabs in the line.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_tab_widths(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column","title":"get_target_document_column","text":"
    get_target_document_column(line_index, x_offset, y_offset)\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:

    Name Type Description Default int

    The index of the line in the document.

    required int

    The x-offset within the wrapped line.

    required int

    The y-offset within the wrapped line (supports negative indexing).

    required

    Returns:

    Type Description int

    The column index corresponding to the line index and y offset.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(line_index)","title":"line_index","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(x_offset)","title":"x_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.get_target_document_column(y_offset)","title":"y_offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.location_to_offset","title":"location_to_offset","text":"
    location_to_offset(location)\n

    Convert a location in the document to an offset within the wrapped/visual display of the document.

    Parameters:

    Name Type Description Default Location

    The location in the document.

    required

    Returns:

    Type Description Offset

    The Offset in the document's visual display corresponding to the given location.

    "},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.location_to_offset(location)","title":"location","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.offset_to_location","title":"offset_to_location","text":"
    offset_to_location(offset)\n

    Given an offset within the wrapped/visual display of the document, return the corresponding location in the document.

    Parameters:

    Name Type Description Default Offset

    The y-offset within the document.

    required

    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.widgets.text_area.WrappedDocument.offset_to_location(offset)","title":"offset","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap","title":"wrap","text":"
    wrap(width, tab_width=None)\n

    Wrap and cache all lines in the document.

    Parameters:

    Name Type Description Default int

    The width to wrap at. 0 for no wrapping.

    required int | None

    The maximum width to consider for tab characters. If None, reuse the tab width.

    None"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap(width)","title":"width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap(tab_width)","title":"tab_width","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range","title":"wrap_range","text":"
    wrap_range(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:

    Name Type Description Default Location

    The start location of the edit that was performed in document-space.

    required Location

    The old end location of the edit in document-space.

    required Location

    The new end location of the edit in document-space.

    required"},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(start)","title":"start","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(old_end)","title":"old_end","text":""},{"location":"widgets/text_area/#textual.widgets.text_area.WrappedDocument.wrap_range(new_end)","title":"new_end","text":""},{"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

    If you wish to change the location of Toasts, it is possible by targeting the ToastRack CSS type. For example:

    ToastRack {\n        align: right top;\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 \u258cIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0 \u258cchecks\u00a0out. \u258c \u258c \u258cPossible\u00a0trap\u00a0detected \u258cNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u00a0 \u258cfully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u00a0battle\u00a0 \u258cstation! \u258c \u258c \u258cIt's\u00a0a\u00a0trap! \u258c \u258c \u258cIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0 \u258cimpersonate\u00a0a\u00a0deity. \u258c

    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","text":"
    Toast(notification)\n

    Bases: Static

    A widget for displaying short-lived notifications.

    Parameters:

    Name Type Description Default Notification

    The notification to show in the toast.

    required"},{"location":"widgets/toast/#textual.widgets._toast.Toast(notification)","title":"notification","text":""},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {'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.

    Bases: Generic[TreeDataType], ScrollView

    A widget for displaying and navigating data in a tree.

    Parameters:

    Name Type Description Default TextType

    The label of the root node of the tree.

    required TreeDataType | None

    The optional data to associate with the root node of the tree.

    None str | None

    The name of the Tree.

    None str | None

    The ID of the tree in the DOM.

    None str | None

    The CSS classes of the tree.

    None bool

    Whether the tree is disabled or not.

    False

    Make non-widget Tree support classes available.

    "},{"location":"widgets/tree/#textual.widgets.Tree(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.Tree(name)","title":"name","text":""},{"location":"widgets/tree/#textual.widgets.Tree(id)","title":"id","text":""},{"location":"widgets/tree/#textual.widgets.Tree(classes)","title":"classes","text":""},{"location":"widgets/tree/#textual.widgets.Tree(disabled)","title":"disabled","text":""},{"location":"widgets/tree/#textual.widgets.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
    BINDINGS = [\n    Binding(\n        \"shift+left\",\n        \"cursor_parent\",\n        \"Cursor to parent\",\n        show=False,\n    ),\n    Binding(\n        \"shift+right\",\n        \"cursor_parent_next_sibling\",\n        \"Cursor to next ancestor\",\n        show=False,\n    ),\n    Binding(\n        \"shift+up\",\n        \"cursor_previous_sibling\",\n        \"Cursor to previous sibling\",\n        show=False,\n    ),\n    Binding(\n        \"shift+down\",\n        \"cursor_next_sibling\",\n        \"Cursor to next sibling\",\n        show=False,\n    ),\n    Binding(\"enter\", \"select_cursor\", \"Select\", show=False),\n    Binding(\"space\", \"toggle_node\", \"Toggle\", show=False),\n    Binding(\n        \"shift+space\",\n        \"toggle_expand_all\",\n        \"Expand or collapse all\",\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]\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.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
    COMPONENT_CLASSES = {\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.ICON_NODE","title":"ICON_NODE class-attribute instance-attribute","text":"
    ICON_NODE = '\u25b6 '\n

    Unicode 'icon' to use for an expandable node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.ICON_NODE_EXPANDED","title":"ICON_NODE_EXPANDED class-attribute instance-attribute","text":"
    ICON_NODE_EXPANDED = '\u25bc '\n

    Unicode 'icon' to use for an expanded node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.auto_expand","title":"auto_expand class-attribute instance-attribute","text":"
    auto_expand = var(True)\n

    Auto expand tree nodes when they are selected.

    "},{"location":"widgets/tree/#textual.widgets.Tree.center_scroll","title":"center_scroll class-attribute instance-attribute","text":"
    center_scroll = var(False)\n

    Keep selected node in the center of the control, where possible.

    "},{"location":"widgets/tree/#textual.widgets.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.cursor_node","title":"cursor_node property","text":"
    cursor_node\n

    The currently selected node, or None if no selection.

    "},{"location":"widgets/tree/#textual.widgets.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.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.last_line","title":"last_line property","text":"
    last_line\n

    The index of the last line.

    "},{"location":"widgets/tree/#textual.widgets.Tree.root","title":"root instance-attribute","text":"
    root = _add_node(None, text_label, data)\n

    The root node of the tree.

    "},{"location":"widgets/tree/#textual.widgets.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.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.NodeCollapsed","title":"NodeCollapsed","text":"
    NodeCollapsed(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.NodeCollapsed.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was collapsed.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeExpanded","title":"NodeExpanded","text":"
    NodeExpanded(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.NodeExpanded.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was expanded.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeHighlighted","title":"NodeHighlighted","text":"
    NodeHighlighted(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.NodeHighlighted.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was highlighted.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeSelected","title":"NodeSelected","text":"
    NodeSelected(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.NodeSelected.control","title":"control property","text":"
    control\n

    The tree that sent the message.

    "},{"location":"widgets/tree/#textual.widgets.Tree.NodeSelected.node","title":"node instance-attribute","text":"
    node = node\n

    The node that was selected.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_down","title":"action_cursor_down","text":"
    action_cursor_down()\n

    Move the cursor down one node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_next_sibling","title":"action_cursor_next_sibling","text":"
    action_cursor_next_sibling()\n

    Move the cursor to the next sibling, or to the paren't sibling if there are no more siblings.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_parent","title":"action_cursor_parent","text":"
    action_cursor_parent()\n

    Move the cursor to the parent node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_parent_next_sibling","title":"action_cursor_parent_next_sibling","text":"
    action_cursor_parent_next_sibling()\n

    Move the cursor to the parent's next sibling.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_previous_sibling","title":"action_cursor_previous_sibling","text":"
    action_cursor_previous_sibling()\n

    Move the cursor to previous sibling, or to the parent if there are no more siblings.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_cursor_up","title":"action_cursor_up","text":"
    action_cursor_up()\n

    Move the cursor up one node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_page_down","title":"action_page_down","text":"
    action_page_down()\n

    Move the cursor down a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_page_up","title":"action_page_up","text":"
    action_page_up()\n

    Move the cursor up a page's-worth of nodes.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_scroll_end","title":"action_scroll_end","text":"
    action_scroll_end()\n

    Move the cursor to the bottom of the tree.

    Note

    Here bottom means vertically, not branch depth.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_scroll_home","title":"action_scroll_home","text":"
    action_scroll_home()\n

    Move the cursor to the top of the tree.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_select_cursor","title":"action_select_cursor","text":"
    action_select_cursor()\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.action_toggle_expand_all","title":"action_toggle_expand_all","text":"
    action_toggle_expand_all()\n

    Expand or collapse all siblings.

    If all the siblings are collapsed then they will be expanded. Otherwise they will all be collapsed.

    "},{"location":"widgets/tree/#textual.widgets.Tree.action_toggle_node","title":"action_toggle_node","text":"
    action_toggle_node()\n

    Toggle the expanded state of the target node.

    "},{"location":"widgets/tree/#textual.widgets.Tree.clear","title":"clear","text":"
    clear()\n

    Clear all nodes under root.

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets.Tree.get_label_width","title":"get_label_width","text":"
    get_label_width(node)\n

    Get the width of the nodes label.

    The default behavior is to call render_label and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    A node.

    required

    Returns:

    Type Description int

    Width in cells.

    "},{"location":"widgets/tree/#textual.widgets.Tree.get_label_width(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.get_node_at_line","title":"get_node_at_line","text":"
    get_node_at_line(line_no)\n

    Get the node for a given line.

    Parameters:

    Name Type Description Default int

    A line number.

    required

    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.get_node_at_line(line_no)","title":"line_no","text":""},{"location":"widgets/tree/#textual.widgets.Tree.get_node_by_id","title":"get_node_by_id","text":"
    get_node_by_id(node_id)\n

    Get a tree node by its ID.

    Parameters:

    Name Type Description Default NodeID

    The ID of the node to get.

    required

    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.get_node_by_id(node_id)","title":"node_id","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor","title":"move_cursor","text":"
    move_cursor(node, animate=False)\n

    Move the cursor to the given node, or reset cursor.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType] | None

    A tree node, or None to reset cursor.

    required bool

    Enable animation

    False"},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line","title":"move_cursor_to_line","text":"
    move_cursor_to_line(line, animate=False)\n

    Move the cursor to the given line.

    Parameters:

    Name Type Description Default int

    The line number (negative indexes are offsets from the last line).

    required

    Enable scrolling animation.

    False

    Raises:

    Type Description IndexError

    If the line doesn't exist.

    "},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line(line)","title":"line","text":""},{"location":"widgets/tree/#textual.widgets.Tree.move_cursor_to_line(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.process_label","title":"process_label","text":"
    process_label(label)\n

    Process a str or Text value into a label.

    May be overridden in a subclass to change how labels are rendered.

    Parameters:

    Name Type Description Default TextType

    Label.

    required

    Returns:

    Type Description Text

    A Rich Text object.

    "},{"location":"widgets/tree/#textual.widgets.Tree.process_label(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label","title":"render_label","text":"
    render_label(node, base_style, style)\n

    Render a label for the given node. Override this to modify how labels are rendered.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    A tree node.

    required Style

    The base style of the widget.

    required Style

    The additional style for the label.

    required

    Returns:

    Type Description Text

    A Rich Text object containing the label.

    "},{"location":"widgets/tree/#textual.widgets.Tree.render_label(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label(base_style)","title":"base_style","text":""},{"location":"widgets/tree/#textual.widgets.Tree.render_label(style)","title":"style","text":""},{"location":"widgets/tree/#textual.widgets.Tree.reset","title":"reset","text":"
    reset(label, data=None)\n

    Clear the tree and reset the root node.

    Parameters:

    Name Type Description Default TextType

    The label for the root node.

    required TreeDataType | None

    Optional data for the root node.

    None

    Returns:

    Type Description Self

    The Tree instance.

    "},{"location":"widgets/tree/#textual.widgets.Tree.reset(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.Tree.reset(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line","title":"scroll_to_line","text":"
    scroll_to_line(line, animate=True)\n

    Scroll to the given line.

    Parameters:

    Name Type Description Default int

    A line number.

    required bool

    Enable animation.

    True"},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line(line)","title":"line","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_line(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node","title":"scroll_to_node","text":"
    scroll_to_node(node, animate=True)\n

    Scroll to the given node.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType]

    Node to scroll in to view.

    required bool

    Animate scrolling.

    True"},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.scroll_to_node(animate)","title":"animate","text":""},{"location":"widgets/tree/#textual.widgets.Tree.select_node","title":"select_node","text":"
    select_node(node)\n

    Move the cursor to the given node and select it, or reset cursor.

    Parameters:

    Name Type Description Default TreeNode[TreeDataType] | None

    A tree node to move the cursor to and select, or None to reset cursor.

    required"},{"location":"widgets/tree/#textual.widgets.Tree.select_node(node)","title":"node","text":""},{"location":"widgets/tree/#textual.widgets.Tree.unselect","title":"unselect","text":"
    unselect()\n

    Hide and reset the cursor.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_cursor_line","title":"validate_cursor_line","text":"
    validate_cursor_line(value)\n

    Prevent cursor line from going outside of range.

    Parameters:

    Name Type Description Default int

    The value to test.

    required Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_cursor_line(value)","title":"value","text":""},{"location":"widgets/tree/#textual.widgets.Tree.validate_guide_depth","title":"validate_guide_depth","text":"
    validate_guide_depth(value)\n

    Restrict guide depth to reasonable range.

    Parameters:

    Name Type Description Default int

    The value to test.

    required Return

    A valid version of the given value.

    "},{"location":"widgets/tree/#textual.widgets.Tree.validate_guide_depth(value)","title":"value","text":""},{"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.AddNodeError","title":"AddNodeError","text":"

    Bases: Exception

    Exception raised when there is an error with a request to add a node.

    "},{"location":"widgets/tree/#textual.widgets.tree.RemoveRootError","title":"RemoveRootError","text":"

    Bases: Exception

    Exception raised when trying to remove the root of a TreeNode.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"TreeNode","text":"
    TreeNode(\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:

    Name Type Description Default Tree[TreeDataType]

    The tree that the node is being attached to.

    required TreeNode[TreeDataType] | None

    The parent node that this node is being attached to.

    required NodeID

    The ID of the node.

    required Text

    The label for the node.

    required TreeDataType | None

    Optional data to associate with the node.

    None bool

    Should the node be attached in an expanded state?

    True bool

    Should the node allow being expanded by the user?

    True"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(tree)","title":"tree","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(parent)","title":"parent","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(id)","title":"id","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(expanded)","title":"expanded","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode(allow_expand)","title":"allow_expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.allow_expand","title":"allow_expand property writable","text":"
    allow_expand\n

    Is this node allowed to expand?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.children","title":"children property","text":"
    children\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\n

    The ID of the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_collapsed","title":"is_collapsed property","text":"
    is_collapsed\n

    Is the node collapsed?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_expanded","title":"is_expanded property","text":"
    is_expanded\n

    Is the node expanded?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.is_last","title":"is_last property","text":"
    is_last\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\n

    Is this node the root of the tree?

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.label","title":"label property writable","text":"
    label\n

    The label for the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.line","title":"line property","text":"
    line\n

    The line number for this node, or -1 if it is not displayed.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.next_sibling","title":"next_sibling property","text":"
    next_sibling\n

    The next sibling below the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.parent","title":"parent property","text":"
    parent\n

    The parent of the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.previous_sibling","title":"previous_sibling property","text":"
    previous_sibling\n

    The previous sibling below the node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.siblings","title":"siblings property","text":"
    siblings\n

    The siblings of this node (includes self).

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.tree","title":"tree property","text":"
    tree\n

    The tree that this node is attached to.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add","title":"add","text":"
    add(\n    label,\n    data=None,\n    *,\n    before=None,\n    after=None,\n    expand=False,\n    allow_expand=True\n)\n

    Add a node to the sub-tree.

    Parameters:

    Name Type Description Default TextType

    The new node's label.

    required TreeDataType | None

    Data associated with the new node.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node before.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node after.

    None bool

    Node should be expanded.

    False bool

    Allow use to expand the node via keyboard or mouse.

    True

    Returns:

    Type Description TreeNode[TreeDataType]

    A new Tree node

    Raises:

    Type Description AddNodeError

    If there is a problem with the addition request.

    Note

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

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(before)","title":"before","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(after)","title":"after","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(expand)","title":"expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add(allow_expand)","title":"allow_expand","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf","title":"add_leaf","text":"
    add_leaf(label, data=None, *, before=None, after=None)\n

    Add a 'leaf' node (a node that can not expand).

    Parameters:

    Name Type Description Default TextType

    Label for the node.

    required TreeDataType | None

    Optional data.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node before.

    None int | TreeNode[TreeDataType] | None

    Optional index or TreeNode to add the node after.

    None

    Returns:

    Type Description TreeNode[TreeDataType]

    New node.

    Raises:

    Type Description AddNodeError

    If there is a problem with the addition request.

    Note

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

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(data)","title":"data","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(before)","title":"before","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.add_leaf(after)","title":"after","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.collapse","title":"collapse","text":"
    collapse()\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","text":"
    collapse_all()\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","text":"
    expand()\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","text":"
    expand_all()\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","text":"
    refresh()\n

    Initiate a refresh (repaint) of this node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.remove","title":"remove","text":"
    remove()\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","text":"
    remove_children()\n

    Remove any child nodes of this node.

    "},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.set_label","title":"set_label","text":"
    set_label(label)\n

    Set a new label for the node.

    Parameters:

    Name Type Description Default TextType

    A str or Text object with the new label.

    required"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.set_label(label)","title":"label","text":""},{"location":"widgets/tree/#textual.widgets.tree.TreeNode.toggle","title":"toggle","text":"
    toggle()\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","text":"
    toggle_all()\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","text":"

    Bases: Exception

    Exception raised when referring to an unknown TreeNode ID.

    "},{"location":"blog/archive/2024/","title":"2024","text":""},{"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/devlog/page/3/","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 230fb63ccc..b5880b762a 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,1102 +2,1102 @@ https://textual.textualize.io/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/FAQ/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/getting_started/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/help/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/roadmap/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/tutorial/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widget_gallery/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/app/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/await_complete/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/await_remove/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/binding/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/cache/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/command/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/constants/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/containers/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/coordinate/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/dom_node/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/errors/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/events/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/filter/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/fuzzy_matcher/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/geometry/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/lazy/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/logger/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/logging/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/map_geometry/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/message/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/message_pump/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/on/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/pilot/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/query/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/reactive/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/renderables/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/screen/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/scroll_view/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/scrollbar/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/signal/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/strip/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/suggester/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/system_commands_source/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/timer/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/types/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/validation/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/walk/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/widget/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/work/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/worker/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/api/worker_manager/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/12/08/be-the-keymaster/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/12/20/a-year-of-building-for-the-terminal/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/06/new-blog/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2024/04/20/behind-the-curtain-of-inline-terminal-applications/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/08/version-040/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/12/11/version-060/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2024/02/20/remote-memory-profiling-with-memray/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/24/spinners-and-progress-bars-in-textual/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/10/04/announcing-textual-plotext/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2024/09/08/towards-textual-web-applications/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/2024/02/11/file-magic-with-the-python-standard-library/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/border/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/hatch/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/horizontal/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/integer/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/keyline/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/name/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/number/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/overflow/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/percentage/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/scalar/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/text_align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/text_style/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/css_types/vertical/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/app_blur/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/app_focus/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/blur/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/click/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/descendant_blur/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/descendant_focus/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/enter/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/focus/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/hide/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/key/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/leave/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/load/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mount/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_capture/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_down/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_move/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_release/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_scroll_down/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_scroll_up/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/mouse_up/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/paste/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/print/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/resize/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/screen_resume/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/screen_suspend/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/show/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/events/unmount/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/examples/styles/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/CSS/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/actions/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/animation/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/app/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/command_palette/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/design/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/devtools/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/events/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/input/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/layout/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/queries/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/reactivity/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/screens/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/styles/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/testing/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/widgets/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/guide/workers/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/center-things/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/design-a-layout/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/package-with-hatch/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/render-and-compose/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/how-to/style-inline-apps/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/reference/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/background/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_subtitle_align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_subtitle_background/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_subtitle_color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_subtitle_style/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_title_align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_title_background/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_title_color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/border_title_style/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/box_sizing/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/content_align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/display/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/dock/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/hatch/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/height/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/keyline/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/layer/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/layers/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/layout/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/margin/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/max_height/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/max_width/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/min_height/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/min_width/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/offset/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/opacity/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/outline/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/overflow/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/padding/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_gutter/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_size/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/text_align/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/text_opacity/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/text_style/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/tint/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/visibility/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/width/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/column_span/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/grid_columns/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/grid_gutter/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/grid_rows/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/grid_size/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/grid/row_span/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_background/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_background_hover/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_color_hover/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_style/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/links/link_style_hover/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_active/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_hover/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_active/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_hover/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/styles/scrollbar_colors/scrollbar_corner_color/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/button/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/checkbox/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/collapsible/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/content_switcher/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/data_table/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/digits/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/directory_tree/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/footer/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/header/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/input/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/label/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/list_item/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/list_view/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/loading_indicator/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/log/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/markdown/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/markdown_viewer/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/masked_input/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/option_list/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/placeholder/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/pretty/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/progress_bar/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/radiobutton/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/radioset/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/rich_log/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/rule/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/select/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/selection_list/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/sparkline/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/static/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/switch/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/tabbed_content/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/tabs/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/text_area/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/toast/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/widgets/tree/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2024/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2023/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2022/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/devlog/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/release/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/news/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/page/2/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/page/3/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/page/4/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2023/page/2/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2023/page/3/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/archive/2022/page/2/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/devlog/page/2/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/devlog/page/3/ - 2024-10-03 + 2024-10-10 https://textual.textualize.io/blog/category/release/page/2/ - 2024-10-03 + 2024-10-10 \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 6ba0dfe7bb..6398ee174b 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/tutorial/index.html b/tutorial/index.html index 46e4086484..66ef1b47c4 100644 --- a/tutorial/index.html +++ b/tutorial/index.html @@ -6869,141 +6869,141 @@

    Stopwatch Application + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch.py + stopwatch.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:16.20 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:12.14 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Stop00:00:08.10 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - d Toggle dark mode  a Add  r Remove ^p palette + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:16.17 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:12.11 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Stop00:00:08.11 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + d Toggle dark mode  a Add  r Remove ^p palette @@ -8470,144 +8470,144 @@

    Reactive attributes + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch05.py + stopwatch05.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.07Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.07Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Start00:00:03.07Reset -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - d Toggle dark mode ^p palette + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.05Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.05Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Start00:00:03.05Reset +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + d Toggle dark mode ^p palette diff --git a/widget_gallery/index.html b/widget_gallery/index.html index e9e52fca33..759e3376db 100644 --- a/widget_gallery/index.html +++ b/widget_gallery/index.html @@ -7720,133 +7720,132 @@

    Digits + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DigitApp + DigitApp - + - - - - - - - - - - -══════════════════════════════════════════════ -╺━┓  ┓ ╻ ╻ ┓  ┏━╸┏━┓╺━┓ ┏━╸┏━╸╺━┓ ┏━╸┏━┓┏━┓╺━┓ - ━┫  ┃ ┗━┫ ┃  ┗━┓┗━┫┏━┛ ┣━┓┗━┓ ━┫ ┗━┓┣━┫┗━┫  ┃ -╺━┛.╺┻╸  ╹╺┻╸,╺━┛╺━┛┗━╸,┗━┛╺━┛╺━┛,╺━┛┗━┛╺━┛  ╹ -══════════════════════════════════════════════ - - - - - - - - - + + + + + + + + + + +══════════════════════════════════════════════ +╶─╮ ╶╮ ╷ ╷╶╮  ╭─╴╭─╮╶─╮ ╭─╴╭─╴╶─╮ ╭─╴╭─╮╭─╮╶─┐ + ─┤  │ ╰─┤ │  ╰─╮╰─┤┌─┘ ├─╮╰─╮ ─┤ ╰─╮├─┤╰─┤  │ +╶─╯.╶┴╴  ╵╶┴╴,╶─╯╶─╯╰─╴,╰─╯╶─╯╶─╯,╶─╯╰─╯╶─╯  ╵ +══════════════════════════════════════════════ + + + + + + + + + diff --git a/widgets/content_switcher/index.html b/widgets/content_switcher/index.html index e318408674..1f699ca8bd 100644 --- a/widgets/content_switcher/index.html +++ b/widgets/content_switcher/index.html @@ -7116,205 +7116,207 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentSwitcherApp + ContentSwitcherApp - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -DataTableMarkdown -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -──────────────────────────────────────────────────────────────────── - - -Three Flavours Cornetto - -The Three Flavours Cornetto trilogy is an anthology series of  -British comedic genre films directed by Edgar Wright. - - -Shaun of the Dead - - -Flavour         UK Release Date         Director           - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  -Strawberry      2004-04-09              Edgar Wright       - - - -Hot Fuzz - - -Flavour       UK Release Date          Director            - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  -Classico      2007-02-17               Edgar Wright        - - - -The World's End - - -Flavour     UK Release Date           Director             - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  -Mint        2013-07-19                Edgar Wright         -▇▇ -──────────────────────────────────────────────────────────────────── + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +DataTableMarkdown +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +──────────────────────────────────────────────────────────────────── + + +Three Flavours Cornetto + +The Three Flavours Cornetto trilogy is an anthology series of  +British comedic genre films directed by Edgar Wright. + + +Shaun of the Dead + + +Flavour         UK Release Date         Director           + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  +Strawberry      2004-04-09              Edgar Wright       + + + +Hot Fuzz + + +Flavour       UK Release Date          Director            + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  +Classico      2007-02-17               Edgar Wright        + + + +The World's End + + +Flavour     UK Release Date           Director             + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  +Mint        2013-07-19                Edgar Wright         +▇▇ +──────────────────────────────────────────────────────────────────── diff --git a/widgets/digits/index.html b/widgets/digits/index.html index 7bdab30408..9a4734c126 100644 --- a/widgets/digits/index.html +++ b/widgets/digits/index.html @@ -6674,7 +6674,7 @@

    DigitsAdded 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 ×. +

    The digits 0-9 and characters A-F are supported, in addition to +, -, ^, :, and ×. 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.

    @@ -6710,133 +6710,132 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DigitApp + DigitApp - + - - - - - - - - - - -══════════════════════════════════════════════ -╺━┓  ┓ ╻ ╻ ┓  ┏━╸┏━┓╺━┓ ┏━╸┏━╸╺━┓ ┏━╸┏━┓┏━┓╺━┓ - ━┫  ┃ ┗━┫ ┃  ┗━┓┗━┫┏━┛ ┣━┓┗━┓ ━┫ ┗━┓┣━┫┗━┫  ┃ -╺━┛.╺┻╸  ╹╺┻╸,╺━┛╺━┛┗━╸,┗━┛╺━┛╺━┛,╺━┛┗━┛╺━┛  ╹ -══════════════════════════════════════════════ - - - - - - - - - + + + + + + + + + + +══════════════════════════════════════════════ +╶─╮ ╶╮ ╷ ╷╶╮  ╭─╴╭─╮╶─╮ ╭─╴╭─╴╶─╮ ╭─╴╭─╮╭─╮╶─┐ + ─┤  │ ╰─┤ │  ╰─╮╰─┤┌─┘ ├─╮╰─╮ ─┤ ╰─╮├─┤╰─┤  │ +╶─╯.╶┴╴  ╵╶┴╴,╶─╯╶─╯╰─╴,╰─╯╶─╯╶─╯,╶─╯╰─╯╶─╯  ╵ +══════════════════════════════════════════════ + + + + + + + + + @@ -6895,132 +6894,131 @@

    Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ClockApp + ClockApp - + - - - - - - - - - - - - ┓  ┓    ┏━┓┏━┓    ┓ ┏━╸ - ┃  ┃  : ┃ ┃┣━┫ :  ┃ ┗━┓ -╺┻╸╺┻╸   ┗━┛┗━┛   ╺┻╸╺━┛ - - - - - - - - - - + + + + + + + + + + + +╶╮ ╷ ╷   ╭─╴╶─┐   ╭─╮╭─╴ + │ ╰─┤ : ╰─╮  │ : │ │╰─╮ +╶┴╴  ╵   ╶─╯  ╵   ╰─╯╶─╯ + + + + + + + + + + @@ -7324,7 +7322,7 @@

    - September 19, 2023 + October 5, 2024