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 โ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Žโ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Žโ–Šโ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–”โ–Ž โ–Š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

Frogmouth https://raw.githubusercontent.com/textualize/frogmouth/main/README.md โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ โ”ƒ๐Ÿ–ผ  Discordโ”ƒContentsLocalBookmarksHistory โ”ƒโ”ƒโ”โ•ธโ”โ”โ”โ”โ”โ”โ”โ”โ•บโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”ƒโ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ”ƒ โ”ƒโ–Žโ–Šโ”ƒโ–ผ โ…  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 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 

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;
}