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 PUThttps://jsonplaceholder.typicode.com/comments/1Send ────────────────── Collection ────────────────────────────────────────────────────────── Request  GET echoHeadersBodyQueryAuthInfoScriptsOptions 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 postUser-ValueAdd ▼ comments/───────User-Agent─────────────────────────────────── GET get comments───────Upgrade-Insecure-Requests─────────────── Response  200 OK  GET get comments (via paBodyHeadersCookiesScriptsTrace >PUT edit a comment━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▼ todos/1   GET get all2  "postId"1,                                                  GET get one3  "name""Updated Commenter Name" ▼ users/4  "email""updated.email@example.com" GET get a user5  "body""This is the updated comment body." GET get all users6  "id"1 ───────────────────────────────7   Edit a commentVisual2:4read-onlyJSONWrapX  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 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 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

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  🐬 Dolphiev6.7.0[R/W] W492F0YXKJ:21728 press ? for commands W492F0YXKJ:21728 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ¹Host Information               System Utilization              InnoDB                    Binary Log              Statistics/s    VersionMySQL 8.0.27             Uptime9 days, 18:05:00  Read Hit96.58%File namemysql-bin.000005Queries21.00K macos11 (x86_64)         CPU89.0%cores 10    Chkpt AgeN/A      Position233551615       SELECT9.60K TypeMySQL                    Load11.97 10.08 7.01  AHI Hit4.85%Size222.73MBINSERT4.20K Uptime9 days, 17:33:10         Memory39.79%BP Instance1        Diff4.75MBUPDATE5.44K Replicas2                        12.73GB/32GBBP Size128MBCache Hit99.96%          DELETE325    Threadscon 13/run 11/cac 0      Swap192KB/1GBBP Available224KBFormatROW (FULL)      REPLACE0      Tablesopen 2.46K/opened 2.79KDiskIOPS R 3.33KBP Dirty61.02MBGTIDON              COMMIT718    Runtime0:01:04 Latency 0.01s    IOPS W 21.32KHistory List220      CompressionOFF             ROLLBACK2      ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ³Metric Graphs SystemDMLBP RequestsHistory ListAHICheckpointRedo LogTable CacheThreadsTemp ObjectsAborted ConnectionsDisk I/O ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CPU % 89  Memory Used 12.73GBIOPS Read 3.33KIOPS Write 21.32KNet Dn 20.84MBNet Up 20.84MB ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 100┤⢕⢕ CPU % ⢀⡠⠊⠒⠤⠤⠤⠤⠔⠤⣀⣀⣀⠤⠒⠒⠒⠒⠣⡀⢀⠤⠤⠒⠒⠒⠒⠒⠒⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠑⠢⢄   32GB├⢕⢕ Memory Used ────────────────────────────────────────────────────────────────────────────────────────────────────Total ⠑⠔⠁⠑⢄⣀⣀⠔⠑⠢⡄⡎⠢⡀⢀⠔⠒⠊⠒⠒⠒⠒⠒⠊⠉⠉⠢⣀⣀⡠⣀ ⠘⡄⡸⠈⠁ ⢠⠃⠘⠇25.60GB┤  80┤ 19.20GB┤  60┤⢀⠟⡄ ⢇⢀⠇ ⢸⢸12.80GB┤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠢⠤⠤⠤⠢⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠔⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠔⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤ ⢀⠇  40┤ ⢀⣀⡠⠔⠊ 6.40GB┤ ⡴⡀⡰⠁ ⣀⣀⡀⢀⡄⠘⢄⡰⠁  20┤⠤⠤⠤⠤⠢⠤⢄⣀⣀⣀⡠⠊⠈⠑⠢⠤⠔⠁⠈⠢⠔⠊⠒⠒⠤⠤⠤⠔⠒⠚⠑⠤⠊      0├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── └┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┬┘└┬─────────────────────────────┬─────────────────────────────┬─────────────────────────────┬─────────────────────────────┬┘ 07/12/24 15:34:0507/12/24 15:34:2007/12/24 15:34:3607/12/24 15:34:5207/12/24 15:35:0807/12/24 15:34:0507/12/24 15:34:2007/12/24 15:34:3607/12/24 15:34:5207/12/24 15:35:08 ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 27.86K┤⢕⢕ IOPS Read  ⢀⠎⠢⡀21.81MB┤⢕⢕ Net Dn ⢀⡠⣀⣀⣀⣀⢀⠤⠤⢄⢀⠔⠑⠢⠤⠤⡄⡠⠤⠴⡀⣀⣀⣀⣀⡀⡠⠤⠔⠤⣀ ⢕⢕ IOPS Write ⣀⣀⣀⡠⠤⠒⠑⠢⠎⠈⠒⠤⠤⠤⣀⣀⡠⠊⠢⣀⣀⡄⢕⢕ Net Up ⣀⣀⡠⠊⠁⠉⠉⠁⠑⠃⠱⡀⣀⡠⠊⠱⡀⢀⠎⠑⢄⢀⠔⠉ ⣀⠤⠊⠘⡄⢀⠾⡀⠑⠊⢠⡆⢀⠤⠤⠤⠊⠑⠊⢸⢠⠃ 22.29K┤⢰⠉⠈⢆⢰⠑⢄⡜⠱⡀⢀⡠⠊⠢⣀⣀⠤⠒⠣⡀⡠⠤⠤⠤⠤⣀⣀17.45MB┤⡸⠸⡀ ⠑⡄⠑⠤⣀⠔⠁⠑⢄⣀⡀⢀⠤⠒⠉⠈⠊⢣⡜ ⠈⠢⡀⢰⠁⠈⢆⠸⣠⠃ 16.71K┤⠑⢄⡎⠈⡆⢰⠁13.08MB┤⠑⢄ ⠸⡇⠑⠇ 11.14K┤ 8.72MB┤ ⡔⢄⢠⠃  5.57K┤⢠⠃⡔⠉⠙⢄ 4.36MB┤ ⢀⠎⢀⠇⠑⠒⠒⠒⠑⠒⠒⠉⠈⠉⠑⠒⠢⠤⢄⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⠔⠉⠢⡀⡠⠢⣀⢀⠎⠑⠤⠤⠤⠤⣀⣀⣀⣀ ⠈⠉⠉⠑⠢⠔⠁⠉⠉⠉⠑⠢⠤⠤⠒⠒⠉⠉⠉⠉⠉⠉⠈⠉⠒⠒⠒⠒⠒⠊      0┤⠒⠒⠒⠒⠑⠒⠒⠒⠤⠤⠤⠔⠤⠤⠤⠤⠤⠔⠤⠤⠤⠤⠤⠤⠤⠤⠤⠢⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠔⠒⠚      0┤⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡇ └┬─────────────────────────────┬──────────────────────────────┬─────────────────────────────┬─────────────────────────────┬┘└┬─────────────────────────────┬─────────────────────────────┬─────────────────────────────┬─────────────────────────────┬┘ 07/12/24 15:34:0507/12/24 15:34:2007/12/24 15:34:3607/12/24 15:34:5207/12/24 15:35:0807/12/24 15:34:0507/12/24 15:34:2007/12/24 15:34:3607/12/24 15:34:5207/12/24 15:35:08 IOPS ReadIOPS WriteNet DnNet Up ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ²Processlist (6)  Thread ID  Protocol  Username              Command   State                 TRX State  R-Lock   R-Mod    Age        Query                                                                                                    1581       SSL/TLS   msandbox              Query     System lock           RUNNING    2        5       00:00:00SELECT s_quantity, s_data, s_dist_08 s_dist FROM stock10 WHERE s_i_id =70489AND s_w_id=5FORUPDATE  1577       SSL/TLS   msandbox              Query     statistics            RUNNING    7        12      00:00:00SELECT i_price, i_name, i_data FROM item8 WHERE i_id =44929  1579       SSL/TLS   msandbox              Query     statistics            RUNNING    2        2       00:00:00SELECT c_id FROM customer7 WHERE c_w_id =4AND c_d_id=3AND c_last='ATIONEINGEING'ORDERBY c_first  1580       SSL/TLS   msandbox              Query     System lock           RUNNING   0000:00:00UPDATE district7 SET d_next_o_id =3152WHERE d_id =5AND d_w_id=2  1582       SSL/TLS   msandbox              Query     System lock           RUNNING    2        5       00:00:00SELECT s_quantity, s_data, s_dist_07 s_dist FROM stock5 WHERE s_i_id =76760AND s_w_id=3FORUPDATE  1583       SSL/TLS   msandbox              Query     statistics           N/A0000:00:00SELECTcount(c_id) namecnt FROM customer7 WHERE c_w_id =2AND c_d_id=1AND c_last='ABLEPRIBAR' ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

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

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