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.


Live Demo

The official Textual demo.


Built with Textual

Textual has enabled an ecosystem of applications and tools for developers and non-developers alike.

Here are a few examples.

Posting

The API client that lives in your terminal. Posting is a beautiful open-source terminal app for developing and testing APIs.

Posting Website

Posting Github Repository

Posting Posting2.3.0darrenburns@arcadia.local PUTโ–ผhttps://jsonplaceholder.typicode.com/comments/1โ– โ– โ– โ– โ– โ– โ– Send โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Collection โ”€โ•ฎโ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Request โ”€โ•ฎ โ”‚GET echoโ”‚โ”‚Headersโ€ขBodyโ€ขQueryโ€ขAuthInfoScriptsOptionsโ”‚ โ”‚GET get random userโ”‚โ”‚โ•ธโ”โ”โ”โ”โ”โ”โ”โ”โ•บโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”‚ โ”‚POS echo postโ”‚โ”‚ Content-Type     application/json     โ”‚ โ”‚โ–ผ jsonplaceholder/โ”‚โ”‚ Referer          https://example.com/ โ”‚ โ”‚โ–ผ posts/โ”‚โ”‚ Accept-Encoding  gzip                 โ”‚ โ”‚GET get allโ”‚โ”‚ Cache-Control    no-cache             โ”‚ โ”‚GET get oneโ”‚โ”‚โ”‚ โ”‚POS createโ”‚โ”‚โ”‚ โ”‚DEL delete a postโ”‚โ”‚User-ValueAddโ”‚ โ”‚โ–ผ comments/โ”‚โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ŽUser-Agentโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚GET get commentsโ”‚โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ŽUpgrade-Insecure-Requestsโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Response  200 OK โ”€โ•ฎ โ”‚GET get comments (via paโ”‚โ”‚BodyHeadersCookiesScriptsTraceโ”‚ โ”‚>PUT edit a commentโ”‚โ”‚โ•ธโ”โ”โ”โ”โ•บโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”‚ โ”‚โ–ผ todos/โ”‚โ”‚1  โ”‚ โ”‚GET get allโ”‚โ”‚2  "postId"1,                                                 โ”‚ โ”‚GET get oneโ”‚โ”‚3  "name""Updated Commenter Name"โ”‚ โ”‚โ–ผ users/โ”‚โ”‚4  "email""updated.email@example.com"โ”‚ โ”‚GET get a userโ”‚โ”‚5  "body""This is the updated comment body."โ”‚ โ”‚GET get all usersโ”‚โ”‚6  "id"1โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”‚7  โ”‚ โ”‚Edit a commentโ”‚โ”‚Visual2:4read-onlyJSONโ–ผWrapโ–Xโ–Œโ”‚ โ•ฐโ”€ sample-collections โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 151.00B in 366.86msโ”€โ•ฏ  ^j Send  ^t Method  ^s Save  ^n New  ^p Commands  ^o Jump  ^c Quit  f1 Help 

Toolong

A terminal application to view, tail, merge, and search log files (plus JSONL).

Toolong Github Repository

UI โ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Žโ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Žโ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Ž โ–Šfeedsโ–Žโ–Šโ–Xโ–Œ Case sensitiveโ–Žโ–Šโ–Xโ–Œ 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

Memray

Memray is a memory profiler for Python, built by Bloomberg.

Memray Github Repository

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

Your single pane of glass for real-time analytics into MySQL/MariaDB & ProxySQL

Dolphie Github Repository

Dolphie


Harlequin

An easy, fast, and beautiful database client for the terminal.

Harlequin website

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 dโ”‚โ”‚10  groupby123โ”‚ โ”‚โ”‚  โ”œโ”€ driverId ##โ”‚โ”‚11  orderbyavg_standing asc                                    โ”‚ โ”‚โ”‚  โ”œโ”€ driverRef sโ”‚โ”‚โ”‚ โ”‚โ”‚  โ”œโ”€ forename sโ”‚โ”‚โ”‚ โ”‚โ”‚  โ”œโ”€ nationality sโ”‚โ”‚โ”‚ โ”‚โ”‚  โ”œโ”€ number sโ”‚โ”‚โ”‚ โ”‚โ”‚  โ”œโ”€ surname sโ”‚โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚โ”‚  โ””โ”€ url sโ”‚โ–Xโ–Œ 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 

Examples

The following examples are taken from the examples directory.

Click the tabs to see the code behind the example.

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: $panel;
    color: $text;
    content-align: center middle;
    text-align: right;
}

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