Skip to content

Tip

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

Click (top left) on mobile.

Welcome

Welcome to the Textual framework documentation.

Get started or go straight to the Tutorial

What is Textual?

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 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ feedsX Case sensitiveX Regex ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 162.71.236.120 - - [29/Jan/2024:13:34:58 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"-""Net 52.70.240.171 - - [29/Jan/2024:13:35:33 +0000]"GET /2007/07/10/postmarkup-105/ HTTP/1.1"3010 121.137.55.45 - - [29/Jan/2024:13:36:19 +0000]"GET /blog/rootblog/feeds/posts/ HTTP/1.1"20010 98.207.26.211 - - [29/Jan/2024:13:36:37 +0000]"GET /feeds/posts HTTP/1.1"3070"-""Mozilla/5. 98.207.26.211 - - [29/Jan/2024:13:36:42 +0000]"GET /feeds/posts/ HTTP/1.1"20098063"-""Mozil 18.183.222.19 - - [29/Jan/2024:13:37:44 +0000]"GET /blog/rootblog/feeds/posts/ HTTP/1.1"20010 66.249.64.164 - - [29/Jan/2024:13:37:46 +0000]"GET /blog/tech/post/a-texture-mapped-spinning-3d 116.203.207.165 - - [29/Jan/2024:13:37:55 +0000]"GET /blog/tech/feeds/posts/ HTTP/1.1"2001182 128.65.195.158 - - [29/Jan/2024:13:38:44 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"https:/ 128.65.195.158 - - [29/Jan/2024:13:38:46 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"https:/ 51.222.253.12 - - [29/Jan/2024:13:41:17 +0000]"GET /blog/tech/post/css-in-the-terminal-with-pyt 154.159.237.77 - - [29/Jan/2024:13:42:28 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"-""Moz 92.247.181.10 - - [29/Jan/2024:13:43:23 +0000]"GET /feed/ HTTP/1.1"200107059"https://www.wil 134.209.40.52 - - [29/Jan/2024:13:43:41 +0000]"GET /blog/tech/feeds/posts/ HTTP/1.1"200118238 192.3.134.205 - - [29/Jan/2024:13:43:55 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"-""Mozi 174.136.108.22 - - [29/Jan/2024:13:44:42 +0000]"GET /feeds/posts/ HTTP/1.1"200107059"-""Tin 64.71.157.117 - - [29/Jan/2024:13:45:16 +0000]"GET /feed/ HTTP/1.1"200107059"-""Feedbin fee 121.137.55.45 - - [29/Jan/2024:13:45:19 +0000]"GET /blog/rootblog/feeds/posts/ HTTP/1.1"20010 216.244.66.233 - - [29/Jan/2024:13:45:22 +0000]"GET /robots.txt HTTP/1.1"200132"-""Mozilla/ 78.82.5.250 - - [29/Jan/2024:13:45:29 +0000]"GET /blog/tech/post/real-working-hyperlinks-in-the 78.82.5.250 - - [29/Jan/2024:13:45:30 +0000]"GET /favicon.ico HTTP/1.1"2005694"https://www.w▁▁ 46.244.252.112 - - [29/Jan/2024:13:46:44 +0000]"GET /blog/tech/feeds/posts/ HTTP/1.1"20011823▁▁ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ f1 Help^t Tail^l Line nos.^g Go to Next PreviousTAIL29/01/2024 13:34:58 • 2540

Frogmouth https://raw.githubusercontent.com/textualize/frogmouth/main/README.md ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🖼  DiscordContentsLocalBookmarksHistory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▼ Ⅰ Frogmouth Frogmouth├── Ⅱ Screenshots ├── Ⅱ Compatibility ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔├── Ⅱ Installing Frogmouth is a Markdown viewer / browser for your terminal, ├── Ⅱ Running built with Textual.├── Ⅱ Features └── Ⅱ Follow this project Frogmouth can open *.md files locally or via a URL. There is a  familiar browser-like navigation stack, history, bookmarks, and table of contents.▅▅ A quick video tour of Frogmouth. https://user-images.githubusercontent.com/554369/235305502-2699 a70e-c9a6-495e-990e-67606d84bbfa.mp4 (thanks Screen Studio) ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                         Screenshots                         ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁                        Compatibility                        ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Frogmouth runs on Linux, macOS, and Windows. Frogmouth requires Python 3.8 or above. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  F1  Help  F2  About  CTRL+N  Navigation  CTRL+Q  Quit 

TUIApp Memray live tracking      Tue Feb 20 13:53:11 2024  (∩`-´)⊃━☆゚.*・。゚  Heap Usage ───────────────────── PID: 77542CMD: memray run --live -m http.server                               ███ TID: 0x1Thread 1 of 1                               ███ Samples: 6Duration: 6.1 seconds                               ███                                ███ ── 1.501MB (100% of 1.501MB max)                        Location                      Total Bytes% TotalOwn Bytes% OwnAllocations  _run_tracker                                           1.440MB 95.94%  1.111KB0.07%        440 memray.comman  _run_module_code                                       1.381MB 91.99%   0.000B0.00%        388 <frozen runpy  _find_and_load                                         1.364MB 90.86% 960.000B0.06%        361 <frozen impor  _load_unlocked                                         1.360MB 90.62%   0.000B0.00%        355 <frozen impor▄▄  exec_module                                            1.355MB 90.28%  1.225KB0.08%        351 <frozen impor  run_module                                             1.351MB 90.00%  1.273KB0.08%        354 <frozen runpy  _run_code                                              1.334MB 88.90% 890.000B0.06%        341 <frozen runpy  _call_with_frames_removed                              1.298MB 86.49%   0.000B0.00%        283 <frozen impor  get_code                                               1.168MB 77.80%   0.000B0.00%        185 <frozen impor  <module>                                               1.095MB 72.96%  1.688KB0.11%         95 http.server    _find_and_load_unlocked                               59.031KB  3.84%   1.000B0.00%         40 <frozen impor  test                                                  42.097KB  2.74%   0.000B0.00%         27 http.server    __init__                                              41.565KB  2.70%   0.000B0.00%         20 socketserver   getfqdn                                               40.933KB  2.66%  2.135KB0.14%         18 socket         server_bind                                           40.933KB  2.66%   0.000B0.00%         18 http.server    search_function                                       38.798KB  2.52%   0.000B0.00%         16 encodings      _handle_fromlist                                      29.723KB  1.93%   0.000B0.00%         33 <frozen impor  <module>                                              24.617KB  1.60%  1.688KB0.11%          6 encodings.idn  _compile                                              23.629KB  1.54%   0.000B0.00%         11 re             Q  Quit  <  Previous Thread  >  Next Thread  T  Sort by Total  O  Sort by Own  A  Sort by Allocations  SPACE  Pause 

Dolphie


Harlequin Data Catalog ───────────── Query Editor ───────────────────────────────────────────────────────────────────────── ▼ f1 db 1  select └─ ▼ main sch 2  drivers.surname,                                          ├─ ▶ circuits t 3  drivers.forename,                                         ├─ ▶ constructor_result 4  drivers.nationality,                                      ├─ ▶ constructor_standi 5  avg(driver_standings.position)asavg_standing,           ├─ ▶ constructors t 6  avg(driver_standings.points)asavg_points ├─ ▶ driver_standings t 7  fromdriver_standings ├─ ▼ drivers t 8  joindriversondriver_standings.driverid=drivers.driverid │  ├─ code s 9  joinracesondriver_standings.raceid=races.raceid │  ├─ dob d10  groupby123 │  ├─ driverId ##11  orderbyavg_standing asc                                     │  ├─ driverRef s │  ├─ forename s │  ├─ nationality s │  ├─ number s │  ├─ surname s──────────────────────────────────────────────────────────────────────────────────────── │  └─ url sX Limit 500Run Query ├─ ▶ lap_times t Query Results (850 Records) ────────────────────────────────────────────────────────── ├─ ▶ pit_stops t surname s forename s nationality s avg_standing #.# av ├─ ▶ qualifying t Hamilton                 Lewis              British            2.66                14 ├─ ▶ races t Prost                    Alain              French             3.51                33 ├─ ▶ results t Stewart                  Jackie             British            3.78                24 ├─ ▶ seasons t Schumacher               Michael            German             4.33                46 ├─ ▶ sprint_results t Verstappen               Max                Dutch              5.09                12 ├─ ▶ status t Fangio                   Juan               Argentine          5.22                16 └─ ▶ tbl1 t Pablo Montoya            Juan               Colombian          5.25                27  Farina                   Nino               Italian            5.27                11  Hulme                    Denny              New Zealander      5.34                14  Fagioli                  Luigi              Italian            5.67                9.  Clark                    Jim                British            5.81                17  Vettel                   Sebastian          German             5.84                10  Senna                    Ayrton             Brazilian          5.92                31 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  CTRL+Q  Quit  F1  Help 

from time import monotonic

from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer
from textual.reactive import reactive
from textual.widgets import Button, Footer, Header, Static


class TimeDisplay(Static):
    """A widget to display elapsed time."""

    start_time = reactive(monotonic)
    time = reactive(0.0)
    total = reactive(0.0)

    def on_mount(self) -> None:
        """Event handler called when widget is added to the app."""
        self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)

    def update_time(self) -> None:
        """Method to update time to current."""
        self.time = self.total + (monotonic() - self.start_time)

    def watch_time(self, time: float) -> None:
        """Called when the time attribute changes."""
        minutes, seconds = divmod(time, 60)
        hours, minutes = divmod(minutes, 60)
        self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}")

    def start(self) -> None:
        """Method to start (or resume) time updating."""
        self.start_time = monotonic()
        self.update_timer.resume()

    def stop(self):
        """Method to stop the time display updating."""
        self.update_timer.pause()
        self.total += monotonic() - self.start_time
        self.time = self.total

    def reset(self):
        """Method to reset the time display to zero."""
        self.total = 0
        self.time = 0


class Stopwatch(Static):
    """A stopwatch widget."""

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

    def compose(self) -> ComposeResult:
        """Create child widgets of a stopwatch."""
        yield Button("Start", id="start", variant="success")
        yield Button("Stop", id="stop", variant="error")
        yield Button("Reset", id="reset")
        yield TimeDisplay()


class StopwatchApp(App):
    """A Textual app to manage stopwatches."""

    CSS_PATH = "stopwatch.tcss"

    BINDINGS = [
        ("d", "toggle_dark", "Toggle dark mode"),
        ("a", "add_stopwatch", "Add"),
        ("r", "remove_stopwatch", "Remove"),
    ]

    def compose(self) -> ComposeResult:
        """Called to add widgets to the app."""
        yield Header()
        yield Footer()
        yield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")

    def action_add_stopwatch(self) -> None:
        """An action to add a timer."""
        new_stopwatch = Stopwatch()
        self.query_one("#timers").mount(new_stopwatch)
        new_stopwatch.scroll_visible()

    def action_remove_stopwatch(self) -> None:
        """Called to remove a timer."""
        timers = self.query("Stopwatch")
        if timers:
            timers.last().remove()

    def action_toggle_dark(self) -> None:
        """An action to toggle dark mode."""
        self.dark = not self.dark


if __name__ == "__main__":
    app = StopwatchApp()
    app.run()
Stopwatch {
    layout: horizontal;
    background: $boost;
    height: 5;
    min-width: 50;
    margin: 1;
    padding: 1;
}

TimeDisplay {
    content-align: center middle;
    text-opacity: 60%;
    height: 3;
}

Button {
    width: 16;
}

#start {
    dock: left;
}

#stop {
    dock: left;
    display: none;
}

#reset {
    dock: right;
}

.started {
    text-style: bold;
    background: $success;
    color: $text;
}

.started TimeDisplay {
    text-opacity: 100%;
}

.started #start {
    display: none
}

.started #stop {
    display: block
}

.started #reset {
    visibility: hidden
}

PrideApp

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


class PrideApp(App):
    """Displays a pride flag."""

    COLORS = ["red", "orange", "yellow", "green", "blue", "purple"]

    def compose(self) -> ComposeResult:
        for color in self.COLORS:
            stripe = Static()
            stripe.styles.height = "1fr"
            stripe.styles.background = color
            yield stripe


if __name__ == "__main__":
    PrideApp().run()

CalculatorApp ╶─╮ ╶╮ ╷ ╷╭─╴╭─╮╶─╮  ─┤  │ ╰─┤╰─╮╰─┤┌─┘ ╶─╯•╶┴╴  ╵╶─╯╶─╯╰─╴ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ C+/-%÷ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 789× ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 456- ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 123+ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 0.= ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

"""
An implementation of a classic calculator, with a layout inspired by macOS calculator.

Works like a real calculator. Click the buttons or press the equivalent keys.
"""

from decimal import Decimal

from textual import events, on
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.css.query import NoMatches
from textual.reactive import var
from textual.widgets import Button, Digits


class CalculatorApp(App):
    """A working 'desktop' calculator."""

    CSS_PATH = "calculator.tcss"

    numbers = var("0")
    show_ac = var(True)
    left = var(Decimal("0"))
    right = var(Decimal("0"))
    value = var("")
    operator = var("plus")

    # Maps button IDs on to the corresponding key name
    NAME_MAP = {
        "asterisk": "multiply",
        "slash": "divide",
        "underscore": "plus-minus",
        "full_stop": "point",
        "plus_minus_sign": "plus-minus",
        "percent_sign": "percent",
        "equals_sign": "equals",
        "minus": "minus",
        "plus": "plus",
    }

    def watch_numbers(self, value: str) -> None:
        """Called when numbers is updated."""
        self.query_one("#numbers", Digits).update(value)

    def compute_show_ac(self) -> bool:
        """Compute switch to show AC or C button"""
        return self.value in ("", "0") and self.numbers == "0"

    def watch_show_ac(self, show_ac: bool) -> None:
        """Called when show_ac changes."""
        self.query_one("#c").display = not show_ac
        self.query_one("#ac").display = show_ac

    def compose(self) -> ComposeResult:
        """Add our buttons."""
        with Container(id="calculator"):
            yield Digits(id="numbers")
            yield Button("AC", id="ac", variant="primary")
            yield Button("C", id="c", variant="primary")
            yield Button("+/-", id="plus-minus", variant="primary")
            yield Button("%", id="percent", variant="primary")
            yield Button("÷", id="divide", variant="warning")
            yield Button("7", id="number-7", classes="number")
            yield Button("8", id="number-8", classes="number")
            yield Button("9", id="number-9", classes="number")
            yield Button("×", id="multiply", variant="warning")
            yield Button("4", id="number-4", classes="number")
            yield Button("5", id="number-5", classes="number")
            yield Button("6", id="number-6", classes="number")
            yield Button("-", id="minus", variant="warning")
            yield Button("1", id="number-1", classes="number")
            yield Button("2", id="number-2", classes="number")
            yield Button("3", id="number-3", classes="number")
            yield Button("+", id="plus", variant="warning")
            yield Button("0", id="number-0", classes="number")
            yield Button(".", id="point")
            yield Button("=", id="equals", variant="warning")

    def on_key(self, event: events.Key) -> None:
        """Called when the user presses a key."""

        def press(button_id: str) -> None:
            """Press a button, should it exist."""
            try:
                self.query_one(f"#{button_id}", Button).press()
            except NoMatches:
                pass

        key = event.key
        if key.isdecimal():
            press(f"number-{key}")
        elif key == "c":
            press("c")
            press("ac")
        else:
            button_id = self.NAME_MAP.get(key)
            if button_id is not None:
                press(self.NAME_MAP.get(key, key))

    @on(Button.Pressed, ".number")
    def number_pressed(self, event: Button.Pressed) -> None:
        """Pressed a number."""
        assert event.button.id is not None
        number = event.button.id.partition("-")[-1]
        self.numbers = self.value = self.value.lstrip("0") + number

    @on(Button.Pressed, "#plus-minus")
    def plus_minus_pressed(self) -> None:
        """Pressed + / -"""
        self.numbers = self.value = str(Decimal(self.value or "0") * -1)

    @on(Button.Pressed, "#percent")
    def percent_pressed(self) -> None:
        """Pressed %"""
        self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100))

    @on(Button.Pressed, "#point")
    def pressed_point(self) -> None:
        """Pressed ."""
        if "." not in self.value:
            self.numbers = self.value = (self.value or "0") + "."

    @on(Button.Pressed, "#ac")
    def pressed_ac(self) -> None:
        """Pressed AC"""
        self.value = ""
        self.left = self.right = Decimal(0)
        self.operator = "plus"
        self.numbers = "0"

    @on(Button.Pressed, "#c")
    def pressed_c(self) -> None:
        """Pressed C"""
        self.value = ""
        self.numbers = "0"

    def _do_math(self) -> None:
        """Does the math: LEFT OPERATOR RIGHT"""
        try:
            if self.operator == "plus":
                self.left += self.right
            elif self.operator == "minus":
                self.left -= self.right
            elif self.operator == "divide":
                self.left /= self.right
            elif self.operator == "multiply":
                self.left *= self.right
            self.numbers = str(self.left)
            self.value = ""
        except Exception:
            self.numbers = "Error"

    @on(Button.Pressed, "#plus,#minus,#divide,#multiply")
    def pressed_op(self, event: Button.Pressed) -> None:
        """Pressed one of the arithmetic operations."""
        self.right = Decimal(self.value or "0")
        self._do_math()
        assert event.button.id is not None
        self.operator = event.button.id

    @on(Button.Pressed, "#equals")
    def pressed_equals(self) -> None:
        """Pressed ="""
        if self.value:
            self.right = Decimal(self.value)
        self._do_math()


if __name__ == "__main__":
    CalculatorApp().run(inline=True)
Screen {
    overflow: auto;
}

#calculator {
    layout: grid;
    grid-size: 4;
    grid-gutter: 1 2;
    grid-columns: 1fr;
    grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr;
    margin: 1 2;
    min-height: 25;
    min-width: 26;
    height: 100%;

    &:inline {
        margin: 0 2;
    }
}

Button {
    width: 100%;
    height: 100%;
}

#numbers {
    column-span: 4;
    padding: 0 1;
    height: 100%;
    background: $primary-lighten-2;
    color: $text;
    content-align: center middle;
    text-align: right;
}

#number-0 {
    column-span: 2;
}