{"id":46300139,"url":"https://github.com/pyratatui/pyratatui","last_synced_at":"2026-04-09T17:16:17.732Z","repository":{"id":342000976,"uuid":"1172376594","full_name":"pyratatui/pyratatui","owner":"pyratatui","description":"🚀🦀⚡  Rust-powered terminal UI for Python — fast, typed, animated, and ergonomic  🔥💎🌈","archived":false,"fork":false,"pushed_at":"2026-03-09T07:27:41.000Z","size":2424,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-09T13:00:19.569Z","etag":null,"topics":["alacritty","beautiful","cargo","crate","docs","interactive","library","maturin","package","pyo3","pypi","python","ratatui","reddit","rust","terminal","trend","tui","ui","wheel"],"latest_commit_sha":null,"homepage":"https://pyratatui.github.io/pyratatui/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pyratatui.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T08:35:49.000Z","updated_at":"2026-03-09T07:21:35.000Z","dependencies_parsed_at":"2026-03-09T11:01:12.549Z","dependency_job_id":null,"html_url":"https://github.com/pyratatui/pyratatui","commit_stats":null,"previous_names":["pyratatui/pyratatui"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/pyratatui/pyratatui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyratatui%2Fpyratatui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyratatui%2Fpyratatui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyratatui%2Fpyratatui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyratatui%2Fpyratatui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pyratatui","download_url":"https://codeload.github.com/pyratatui/pyratatui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyratatui%2Fpyratatui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30332853,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T05:25:20.737Z","status":"ssl_error","status_checked_at":"2026-03-10T05:25:17.430Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alacritty","beautiful","cargo","crate","docs","interactive","library","maturin","package","pyo3","pypi","python","ratatui","reddit","rust","terminal","trend","tui","ui","wheel"],"created_at":"2026-03-04T11:00:26.889Z","updated_at":"2026-04-09T17:16:17.724Z","avatar_url":"https://github.com/pyratatui.png","language":"Rust","funding_links":[],"categories":["📦 Libraries"],"sub_categories":["🔗 Bindings"],"readme":"\u003cdiv align=\"center\"\u003e\n\n# 🐀 PyRatatui\n\n**Professional Python bindings for [ratatui](https://ratatui.rs) 0.30 — powered by Rust \u0026 PyO3**\n\n[![IdeaCred](https://ideacred.com/api/badge/pyratatui/pyratatui?style=for-the-badge)](https://ideacred.com/profile/pyratatui)\n\n[![PyPI](https://img.shields.io/badge/PyPI-pyratatui-orange?style=for-the-badge\u0026logo=pypi\u0026logoColor=white)](https://pypi.org/project/pyratatui/)\n[![Python](https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge\u0026logo=python\u0026logoColor=white)](https://pypi.org/project/pyratatui/)\n![Downloads](https://img.shields.io/pypi/dm/pyratatui?style=for-the-badge\u0026logo=python\u0026label=downloads)\n[![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge\u0026logo=opensourceinitiative\u0026logoColor=white)](LICENSE)\n[![Ratatui](https://img.shields.io/badge/Ratatui-0.30-blueviolet?style=for-the-badge\u0026logo=rust\u0026logoColor=white)](https://github.com/ratatui/ratatui)\n[![PyO3](https://img.shields.io/badge/PyO3-0.28.2-blue?style=for-the-badge\u0026logo=rust\u0026logoColor=white)](https://pyo3.rs)\n[![Platforms](https://img.shields.io/badge/Platforms-Linux%20%7C%20macOS%20%7C%20Windows-aqua?style=for-the-badge)](https://github.com/pyratatui/pyratatui)\n[![Asyncio](https://img.shields.io/badge/asyncio-ready-teal?style=for-the-badge\u0026logo=python\u0026logoColor=white)](https://docs.python.org/3/library/asyncio.html)\n\n*Build rich, high-performance terminal UIs in Python — with the full power of Rust under the hood.*\n\n[**Quickstart**](#quickstart) · [**Installation**](#installation) · [**Widgets**](#widget-reference) · [**Effects**](#tachyonfx-effects) · [**Examples**](#examples) · [**API Reference**](#api-reference) · [**Docs**](https://pyratatui.github.io/pyratatui)\n\n\u003c/div\u003e\n\n---\n\n## 🖼️ Gallery\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_1.png\" alt=\"Widget showcase\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_2.png\" alt=\"Layout panels\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_3.png\" alt=\"Styled text\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_4.png\" alt=\"List navigation\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_5.png\" alt=\"Progress bars\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_6.png\" alt=\"Dynamic table\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_7.png\" alt=\"Async reactive UI\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_8.png\" alt=\"TachyonFX effects\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_9.png\" alt=\"Effect DSL\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_10.png\" alt=\"Full app dashboard\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_11.png\" alt=\"Popup widget\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_12.png\" alt=\"Draggable popup\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_13.png\" alt=\"Scrollable popup\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_14.png\" alt=\"TextArea editor\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_15.png\" alt=\"ScrollView\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_16.png\" alt=\"PieChart\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_17.png\" alt=\"Chart\" width=\"100%\"/\u003e\u003c/td\u003e\n    \u003ctd align=\"center\" width=\"33%\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/pyratatui/pyratatui/main/gallery/snip_18.png\" alt=\"Menu\" width=\"100%\"/\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## What is PyRatatui?\n\nPyRatatui exposes the entire [ratatui](https://ratatui.rs) Rust TUI library to Python via a thin, zero-overhead [PyO3](https://pyo3.rs) extension module. You get:\n\n- **Pixel-perfect terminal rendering** from ratatui's battle-tested Rust layout engine\n- **35+ widgets** out of the box: gauges, tables, trees, menus, charts, calendars, QR codes, images, markdown, and more\n- **TachyonFX animations** — fade, sweep, glitch, dissolve, and composable effect pipelines\n- **Async-native** — `AsyncTerminal` + `asyncio` integration for live, reactive UIs\n- **Full type stubs** — every class and method ships with `.pyi` annotations for IDE autocomplete\n- **Cross-platform** — Linux, macOS, and Windows (pre-built wheels on PyPI for all three)\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quickstart](#quickstart)\n- [Core Concepts](#core-concepts)\n- [Widget Reference](#widget-reference)\n- [TachyonFX Effects](#tachyonfx-effects)\n- [Async \u0026 Reactive UIs](#async--reactive-uis)\n- [CLI Tool](#cli-tool)\n- [API Reference](#api-reference)\n- [Examples](#examples)\n- [Building from Source](#building-from-source)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Installation\n\n### Recommended — Pre-built Wheel\n\n```bash\npip install pyratatui\n```\n\nPre-built wheels are published to PyPI for:\n\n- Linux x86\\_64 (manylinux2014)\n- Linux x86\\_64 and aarch64 (musllinux\\_1\\_2) (starting from v0.2.3)\n- macOS x86\\_64 (starting from v0.2.2) and arm64 (universal2)\n- Windows x86\\_64\n\nIf no wheel exists for your platform, `pip` will automatically compile from source (requires Rust — see [Building from Source](#building-from-source)).\n\n### Virtual Environment (Best Practice)\n\n```bash\npython -m venv .venv\nsource .venv/bin/activate        # Linux / macOS\n# .venv\\Scripts\\activate         # Windows PowerShell\n\npip install pyratatui\n```\n\n### Requirements\n\n| Requirement | Minimum | Notes |\n|---|---|---|\n| Python | 3.10 | 3.11+ recommended |\n| OS | Linux, macOS, Windows | crossterm backend |\n| Rust | 1.75 | source builds only |\n\n### Verify\n\n```python\nimport pyratatui\nprint(pyratatui.__version__)          # \"0.2.7\"\nprint(pyratatui.__ratatui_version__)  # \"0.30\"\n```\n\n---\n\n## Quickstart\n\n### Hello World\n\n```python\nfrom pyratatui import Block, Color, Paragraph, Style, Terminal\n\nwith Terminal() as term:\n    while True:\n        def ui(frame):\n            frame.render_widget(\n                Paragraph.from_string(\"Hello, pyratatui! 🐀  Press q to quit.\")\n                    .block(Block().bordered().title(\"Hello World\"))\n                    .style(Style().fg(Color.cyan())),\n                frame.area,\n            )\n        term.draw(ui)\n        ev = term.poll_event(timeout_ms=100)\n        if ev and ev.code == \"q\":\n            break\n```\n\nOutput:\n```\n┌ Hello World ────────────────────────────────────────────┐\n│ Hello, pyratatui! 🐀  Press q to quit.                  │\n└─────────────────────────────────────────────────────────┘\n```\n\n### Scaffold a New Project\n\n```bash\npyratatui init my_app\ncd my_app\npip install -r requirements.txt\npython main.py\n```\n\n### Ultra-Minimal — `run_app` Helper\n\n```python\nfrom pyratatui import Paragraph, run_app\n\ndef ui(frame):\n    frame.render_widget(\n        Paragraph.from_string(\"Hello! Press q to quit.\"),\n        frame.area,\n    )\n\nrun_app(ui)\n```\n\n---\n\n## Core Concepts\n\n### Terminal \u0026 Frame\n\n`Terminal` is the entry point. Use it as a context manager — it saves the terminal state, enters alternate screen mode, enables raw input, and restores everything on exit (even after exceptions).\n\n`frame` is not a global variable and you never construct it yourself. Each call to `term.draw(...)`, `AsyncTerminal.draw(...)`, `run_app(...)`, or `run_app_async(...)` creates a temporary `Frame` for that render pass and passes it into your callback. Use it only inside that callback.\n\n```python\nwith Terminal() as term:\n    term.draw(lambda frame: ...)    # pyratatui creates frame and passes it in\n    ev = term.poll_event(timeout_ms=50)  # KeyEvent | None\n```\n\n`Frame` holds the drawable area and all render methods for the current pass:\n\n```python\ndef ui(frame):\n    area = frame.area  # Rect — full terminal size\n    frame.render_widget(widget, area)\n```\n\n### Layout\n\n`Layout` divides a `Rect` into child regions using constraints:\n\n```python\nfrom pyratatui import (\n    Block,\n    Constraint,\n    Direction,\n    Layout,\n    Paragraph,\n    run_app,\n)\n\ndef ui(frame):\n    header, body, footer = (\n        Layout()\n        .direction(Direction.Vertical)\n        .constraints([\n            Constraint.length(3),   # fixed 3 rows\n            Constraint.fill(1),     # takes remaining space\n            Constraint.length(1),   # fixed 1 row\n        ])\n        .split(frame.area)\n    )\n\n    frame.render_widget(Block().bordered().title(\"Header\"), header)\n    frame.render_widget(\n        Paragraph.from_string(\"Main content\").block(Block().bordered().title(\"Body\")),\n        body,\n    )\n    frame.render_widget(Paragraph.from_string(\"Press q to quit\"), footer)\n\nrun_app(ui)\n```\n\n**Constraint types:**\n\n| Constraint | Description |\n|---|---|\n| `Constraint.length(n)` | Exactly `n` rows/columns |\n| `Constraint.percentage(pct)` | `pct`% of available space |\n| `Constraint.fill(n)` | Fill remaining space (proportionally weighted) |\n| `Constraint.min(n)` | At least `n` rows/columns |\n| `Constraint.max(n)` | At most `n` rows/columns |\n| `Constraint.ratio(num, den)` | Fractional proportion |\n\n### Styling\n\nAll styling flows through `Style`, `Color`, and `Modifier`:\n\n```python\nfrom pyratatui import Style, Color, Modifier\n\nstyle = (\n    Style()\n    .fg(Color.cyan())\n    .bg(Color.rgb(30, 30, 46))\n    .bold()\n    .italic()\n)\n\n# Named colors\nColor.red()    Color.green()    Color.yellow()\nColor.blue()   Color.magenta()  Color.cyan()\nColor.white()  Color.gray()     Color.dark_gray()\n# Light variants: Color.light_red(), Color.light_green(), ...\n# 256-color: Color.indexed(42)\n# True-color: Color.rgb(255, 100, 0)\n```\n\n### Text Hierarchy\n\nText is composed bottom-up: `Span` → `Line` → `Text`:\n\n```python\nfrom pyratatui import Block, Color, Line, Paragraph, Span, Style, Text, run_app\n\ndef ui(frame):\n    text = Text([\n        Line([\n            Span(\"Status: \", Style().bold()),\n            Span(\"OK\", Style().fg(Color.green())),\n            Span(\"  |  99.9%\", Style().fg(Color.cyan())),\n        ]),\n        Line.from_string(\"Plain text line\"),\n        Line.from_string(\"Right-aligned\").right_aligned(),\n    ])\n\n    frame.render_widget(\n        Paragraph(text).block(Block().bordered().title(\"Text Hierarchy\")),\n        frame.area,\n    )\n\nrun_app(ui)\n```\n\n### Key Events\n\n```python\nev = term.poll_event(timeout_ms=100)\nif ev:\n    print(ev.code)   # \"q\", \"Enter\", \"Up\", \"Down\", \"F1\", etc.\n    print(ev.ctrl)   # True if Ctrl held\n    print(ev.alt)    # True if Alt held\n    print(ev.shift)  # True if Shift held\n\n# Common key codes\n# Letters/digits: \"a\", \"Z\", \"5\"\n# Special:        \"Enter\", \"Esc\", \"Backspace\", \"Tab\", \"BackTab\"\n# Arrows:         \"Up\", \"Down\", \"Left\", \"Right\"\n# Function:       \"F1\" … \"F12\"\n# Ctrl+C:         ev.code == \"c\" and ev.ctrl\n```\n\n\u003e **Tip — Closure Capture:** Always snapshot mutable state into default arguments to avoid late-binding issues in fast render loops:\n\u003e ```python\n\u003e count = state[\"count\"]\n\u003e def ui(frame, _count=count):  # ← captured by value, not reference\n\u003e     ...\n\u003e ```\n\n---\n\n## Widget Reference\n\n### Standard Widgets\n\n| Widget | Description |\n|---|---|\n| `Paragraph` | Single or multi-line text, wrapping, scrolling |\n| `Block` | Bordered container with title, padding, and style |\n| `List` + `ListState` | Scrollable, selectable list |\n| `Table` + `TableState` | Multi-column table with header and footer |\n| `Gauge` | Filled progress bar |\n| `LineGauge` | Single-line progress indicator |\n| `BarChart` | Grouped vertical bar chart |\n| `Sparkline` | Inline sparkline trend chart |\n| `Scrollbar` + `ScrollbarState` | Attach scrollbars to any widget |\n| `Tabs` | Tabbed navigation bar |\n| `Clear` | Clears a rectangular area (use under popups) |\n| `RatatuiMascot` | Ratatui mascot widget (easter egg) |\n\n**Runnable widget gallery:**\n\n```python\nfrom pyratatui import (\n    Block,\n    Color,\n    Constraint,\n    Direction,\n    Gauge,\n    Layout,\n    List,\n    ListItem,\n    ListState,\n    Row,\n    Sparkline,\n    Style,\n    Table,\n    TableState,\n    Tabs,\n    run_app,\n)\n\nlist_state = ListState()\nlist_state.select(0)\n\ntable_state = TableState()\ntable_state.select(0)\n\ndef ui(frame, _list_state=list_state, _table_state=table_state):\n    rows = (\n        Layout()\n        .direction(Direction.Vertical)\n        .constraints([\n            Constraint.length(3),\n            Constraint.length(3),\n            Constraint.fill(1),\n            Constraint.length(5),\n        ])\n        .split(frame.area)\n    )\n    middle = (\n        Layout()\n        .direction(Direction.Horizontal)\n        .constraints([Constraint.percentage(40), Constraint.fill(1)])\n        .split(rows[2])\n    )\n\n    frame.render_widget(\n        Tabs([\"Overview\", \"Logs\", \"Config\"])\n        .select(1)\n        .block(Block().bordered().title(\"Tabs\"))\n        .highlight_style(Style().fg(Color.yellow()).bold()),\n        rows[0],\n    )\n\n    frame.render_widget(\n        Gauge()\n        .percent(75)\n        .label(\"CPU 75%\")\n        .style(Style().fg(Color.green()))\n        .block(Block().bordered().title(\"Gauge\")),\n        rows[1],\n    )\n\n    items = [ListItem(s) for s in [\"Alpha\", \"Beta\", \"Gamma\"]]\n    frame.render_stateful_list(\n        List(items)\n        .block(Block().bordered().title(\"List\"))\n        .highlight_style(Style().fg(Color.yellow()).bold())\n        .highlight_symbol(\"▶ \"),\n        middle[0],\n        _list_state,\n    )\n\n    header = Row.from_strings([\"Name\", \"Status\", \"Uptime\"]).style(\n        Style().fg(Color.cyan()).bold()\n    )\n    table_rows = [\n        Row.from_strings([\"nginx\", \"running\", \"14d\"]),\n        Row.from_strings([\"postgres\", \"running\", \"21d\"]),\n        Row.from_strings([\"redis\", \"degraded\", \"3h\"]),\n    ]\n    frame.render_stateful_table(\n        Table(\n            table_rows,\n            [Constraint.fill(1), Constraint.length(10), Constraint.length(8)],\n            header=header,\n        )\n        .block(Block().bordered().title(\"Table\"))\n        .highlight_style(Style().fg(Color.yellow()).bold())\n        .highlight_symbol(\"▶ \"),\n        middle[1],\n        _table_state,\n    )\n\n    frame.render_widget(\n        Sparkline()\n        .data([10, 40, 20, 80, 55, 90])\n        .max(100)\n        .style(Style().fg(Color.cyan()))\n        .block(Block().bordered().title(\"Sparkline\")),\n        rows[3],\n    )\n\nrun_app(ui)\n```\n\n### Third-Party Widgets\n\n| Widget | Crate | Description |\n|---|---|---|\n| `Popup` / `PopupState` | `tui-popup` | Centered or draggable popups |\n| `TextArea` | `tui-textarea` | Full multi-line editor (Emacs keybindings, undo/redo) |\n| `ScrollView` / `ScrollViewState` | `tui-scrollview` | Scrollable virtual viewport |\n| `QrCodeWidget` | `tui-qrcode` | QR codes rendered in Unicode block characters |\n| `Monthly` / `CalendarDate` | `ratatui widget-calendar` | Monthly calendar with event styling |\n| `BarGraph` | `tui-bar-graph` | Gradient braille/block bar graphs |\n| `Tree` / `TreeState` | `tui-tree-widget` | Collapsible tree view |\n| `TuiLoggerWidget` | `tui-logger` | Live scrolling log viewer |\n| `ImageWidget` / `ImagePicker` | `ratatui-image` | Terminal image rendering |\n| `Canvas` | `ratatui` | Low-level line/point/rect drawing |\n| `Map` | `ratatui` | World map widget |\n| `Button` | built-in | Focus-aware interactive button |\n| `Throbber` | `throbber-widgets-tui` | Animated spinner/progress indicator |\n| `Menu` / `MenuState` | `tui-menu` | Nested dropdown menus with event handling |\n| `PieChart` / `PieData` / `PieStyle` | `tui-piechart` | Pie chart widget with legend and percentages |\n| `Checkbox` | `tui-checkbox` | Configurable checkbox widget |\n| `Chart` / `Dataset` / `Axis` | `ratatui` | Multi-dataset cartesian chart (line/scatter/bar) |\n\n#### Image Rendering\n\n`ImageWidget` supports Kitty, Sixel, iTerm2 inline images, and a Unicode\nhalf-block fallback. For best clarity, call `ImagePicker.from_query()` inside\n`Terminal()` to auto-detect the best protocol and cell size, and the renderer\nuses a high-quality Lanczos3 resampling filter when resizing.\n\n**Third-party widget gallery:**\n\n```python\nfrom pyratatui import (\n    BarColorMode,\n    BarGraph,\n    BarGraphStyle,\n    Block,\n    CalendarDate,\n    CalendarEventStore,\n    Color,\n    Constraint,\n    Direction,\n    Layout,\n    Monthly,\n    Paragraph,\n    Popup,\n    PopupState,\n    QrCodeWidget,\n    QrColors,\n    Style,\n    TextArea,\n    Tree,\n    TreeItem,\n    TreeState,\n    markdown_to_text,\n    run_app,\n)\n\npopup = Popup(\"Press q to dismiss\").title(\"Popup\").style(Style().bg(Color.blue()))\npopup_state = PopupState()\n\ntextarea = TextArea.from_lines([\"Hello\", \"World\"])\ntextarea.set_block(Block().bordered().title(\"TextArea\"))\n\ntree = Tree([\n    TreeItem(\"src\", [TreeItem(\"main.rs\"), TreeItem(\"lib.rs\")]),\n    TreeItem(\"Cargo.toml\"),\n]).block(Block().bordered().title(\"Tree\"))\ntree_state = TreeState()\ntree_state.select([0])\n\ndef ui(frame, _popup_state=popup_state, _ta=textarea, _tree=tree, _tree_state=tree_state):\n    rows = (\n        Layout()\n        .direction(Direction.Vertical)\n        .constraints([\n            Constraint.length(12),\n            Constraint.length(10),\n            Constraint.fill(1),\n        ])\n        .split(frame.area)\n    )\n    top = (\n        Layout()\n        .direction(Direction.Horizontal)\n        .constraints([\n            Constraint.percentage(25),\n            Constraint.percentage(25),\n            Constraint.percentage(25),\n            Constraint.fill(1),\n        ])\n        .split(rows[0])\n    )\n    middle = (\n        Layout()\n        .direction(Direction.Horizontal)\n        .constraints([Constraint.fill(1), Constraint.length(28)])\n        .split(rows[1])\n    )\n\n    qr_block = Block().bordered().title(\"QR Code\")\n    frame.render_widget(qr_block, top[0])\n    frame.render_qrcode(\n        QrCodeWidget(\"https://ratatui.rs\").colors(QrColors.Inverted),\n        qr_block.inner(top[0]),\n    )\n\n    store = CalendarEventStore.today_highlighted(Style().fg(Color.green()).bold())\n    frame.render_widget(\n        Monthly(CalendarDate.today(), store)\n        .block(Block().bordered().title(\"Calendar\"))\n        .show_month_header(Style().bold())\n        .show_weekdays_header(Style().italic()),\n        top[1],\n    )\n\n    graph_block = Block().bordered().title(\"Bar Graph\")\n    frame.render_widget(graph_block, top[2])\n    frame.render_widget(\n        BarGraph([0.1, 0.4, 0.9, 0.6, 0.8])\n        .bar_style(BarGraphStyle.Braille)\n        .color_mode(BarColorMode.VerticalGradient)\n        .gradient(\"turbo\"),\n        graph_block.inner(top[2]),\n    )\n\n    frame.render_stateful_popup(popup, top[3], _popup_state)\n\n    frame.render_widget(\n        Paragraph(markdown_to_text(\"# Hello\\n\\n**bold** _italic_ `code`\"))\n        .block(Block().bordered().title(\"Markdown\")),\n        middle[0],\n    )\n    frame.render_stateful_tree(_tree, middle[1], _tree_state)\n\n    frame.render_textarea(_ta, rows[2])\n\nrun_app(ui)\n```\n\n---\n\n## TachyonFX Effects\n\nPyRatatui ships the full [tachyonfx](https://github.com/junkdog/tachyonfx) effects engine. Effects are post-render transforms that mutate the frame buffer — always apply them *after* rendering your widgets.\n\n### Effect Types\n\n| Effect | Description |\n|---|---|\n| `Effect.fade_from_fg(color, ms)` | Fade text from a color into its natural color |\n| `Effect.fade_to_fg(color, ms)` | Fade text out to a flat color |\n| `Effect.fade_from(bg, fg, ms)` | Fade both background and foreground from color |\n| `Effect.fade_to(bg, fg, ms)` | Fade both background and foreground to color |\n| `Effect.coalesce(ms)` | Characters materialize in from random positions |\n| `Effect.dissolve(ms)` | Characters scatter and dissolve |\n| `Effect.slide_in(direction, ms)` | Slide content in from an edge |\n| `Effect.slide_out(direction, ms)` | Slide content out to an edge |\n| `Effect.sweep_in(dir, span, grad, color, ms)` | Gradient sweep reveal |\n| `Effect.sweep_out(dir, span, grad, color, ms)` | Gradient sweep hide |\n| `Effect.sequence(effects)` | Run effects one after another |\n| `Effect.parallel(effects)` | Run effects simultaneously |\n| `Effect.sleep(ms)` | Delay before next effect in a sequence |\n| `Effect.repeat(effect, times=-1)` | Loop an effect (−1 = forever) |\n| `Effect.ping_pong(effect)` | Play an effect forward then backward |\n| `Effect.never_complete(effect)` | Keep an effect alive indefinitely |\n\n### Interpolations\n\n`Interpolation.Linear`, `QuadIn/Out/InOut`, `CubicIn/Out/InOut`, `SineIn/Out/InOut`,\n`CircIn/Out/InOut`, `ExpoIn/Out/InOut`, `ElasticIn/Out`, `BounceIn/Out/BounceInOut`, `BackIn/Out/BackInOut`\n\n### Basic Effect Usage\n\n```python\nimport time\nfrom pyratatui import Effect, EffectManager, Interpolation, Color, Terminal, Paragraph\n\nmgr = EffectManager()\nmgr.add(Effect.fade_from_fg(Color.black(), 1000, Interpolation.SineOut))\nlast = time.monotonic()\n\nwith Terminal() as term:\n    while not (ev := term.poll_event(timeout_ms=16)) or ev.code != \"q\":\n        now = time.monotonic()\n        elapsed_ms = int((now - last) * 1000)\n        last = now\n\n        def ui(frame, _mgr=mgr, _ms=elapsed_ms):\n            # Step 1 — render widgets\n            frame.render_widget(Paragraph.from_string(\"Fading in…\"), frame.area)\n            # Step 2 — apply effects to the same buffer\n            frame.apply_effect_manager(_mgr, _ms, frame.area)\n\n        term.draw(ui)\n```\n\n### Effect DSL\n\nCompile tachyonfx expressions at runtime — perfect for config-driven or user-customisable animations:\n\n```python\nfrom pyratatui import compile_effect, EffectManager\n\n# DSL mirrors the Rust / tachyonfx expression syntax\neffect = compile_effect(\"fx::coalesce(500)\")\neffect = compile_effect(\"fx::dissolve((800, BounceOut))\")\neffect = compile_effect(\"fx::fade_from_fg(Color::Black, (600, QuadOut))\")\neffect = compile_effect(\"fx::sweep_in(LeftToRight, 10, 5, Color::Black, (700, SineOut))\")\n\nmgr = EffectManager()\nmgr.add(effect)\n```\n\n### Cell Filters\n\nTarget effects at specific cells:\n\n```python\nfrom pyratatui import CellFilter, Effect, Color\n\neffect = Effect.fade_from_fg(Color.black(), 800)\neffect.with_filter(CellFilter.text())                           # text cells only\neffect.with_filter(CellFilter.inner(horizontal=1, vertical=1)) # inner area\neffect.with_filter(CellFilter.fg_color(Color.cyan()))          # specific fg color\neffect.with_filter(CellFilter.any_of([CellFilter.text(), CellFilter.all()]))\n```\n\n---\n\n## Async \u0026 Reactive UIs\n\nUse `AsyncTerminal` to combine rendering with background `asyncio` tasks:\n\n```python\nimport asyncio\nfrom pyratatui import AsyncTerminal, Gauge, Block, Style, Color\n\nstate = {\"progress\": 0}\n\nasync def background_worker():\n    while state[\"progress\"] \u003c 100:\n        await asyncio.sleep(0.1)\n        state[\"progress\"] += 2\n\nasync def main():\n    worker = asyncio.create_task(background_worker())\n\n    async with AsyncTerminal() as term:\n        async for ev in term.events(fps=30):\n            pct = state[\"progress\"]\n\n            def ui(frame, _pct=pct):\n                frame.render_widget(\n                    Gauge()\n                    .percent(_pct)\n                    .label(f\"Loading… {_pct}%\")\n                    .style(Style().fg(Color.green()))\n                    .block(Block().bordered().title(\"Progress\")),\n                    frame.area,\n                )\n\n            term.draw(ui)\n\n            if ev and ev.code == \"q\":\n                break\n            if pct \u003e= 100:\n                break\n\n    worker.cancel()\n\nasyncio.run(main())\n```\n\n### `AsyncTerminal.events()` Parameters\n\nBy default `events()` keeps yielding each tick; pass `stop_on_quit=True` to opt into automatic exit on `q`/Ctrl+C.\n\n```python\nasync for ev in term.events(fps=30.0, stop_on_quit=True):\n    # ev is KeyEvent | None\n    # None emitted each tick (use for animations / periodic updates)\n    # stop_on_quit=True (opt-in) exits the loop automatically on \"q\" or Ctrl+C\n```\n\n### `run_app` / `run_app_async` Helpers\n\nFor simpler apps that don't need manual task management; keep in mind that quitting must be implemented via `on_key` or another explicit signal.\n\n```python\nfrom pyratatui import run_app, run_app_async, Paragraph\n\n# Synchronous\ndef ui(frame):\n    frame.render_widget(\n        Paragraph.from_string(\"Hello!\"),\n        frame.area\n    )\n\nrun_app(ui, on_key=lambda ev: ev.code == \"q\")\n\n# Asynchronous\nimport asyncio\n\nasync def main():\n    tick = 0\n    def ui(frame):\n        nonlocal tick\n        frame.render_widget(Paragraph.from_string(f\"Tick: {tick}\"), frame.area)\n        tick += 1\n    await run_app_async(ui, fps=30, on_key=lambda ev: ev.code == \"q\")\n\nasyncio.run(main())\n```\n\n---\n\n## CLI Tool\n\nPyRatatui ships a `pyratatui` CLI for project scaffolding and version inspection.\n\n```\nUsage: pyratatui [COMMAND]\n\nCommands:\n  init     Create a new PyRatatui project scaffold\n  version  Show PyRatatui version\n\nOptions:\n  --help   Show help message\n```\n\n### `pyratatui init`\n\n```bash\npyratatui init my_tui_app [--verbose]\n```\n\nCreates a ready-to-run project:\n\n```\nmy_tui_app/\n├── main.py           # runnable hello world starter\n├── pyproject.toml    # app metdata\n├── .gitignore        # skip unnecessary files from commit\n└── README.md         # project docs\n```\n\n```bash\ncd my_tui_app\npip install -r requirements.txt\npython main.py\n```\n\n### `pyratatui version`\n\n```bash\npyratatui version\n# PyRatatui 0.2.7\n```\n\n---\n\n## API Reference\n\n### Terminal\n\n```python\nclass Terminal:\n    def __enter__(self) -\u003e Terminal\n    def __exit__(self, ...) -\u003e bool\n    def draw(self, draw_fn: Callable[[Frame], None]) -\u003e None\n    def poll_event(self, timeout_ms: int = 0) -\u003e KeyEvent | None\n    def area(self) -\u003e Rect\n    def clear(self) -\u003e None\n    def hide_cursor(self) -\u003e None\n    def show_cursor(self) -\u003e None\n    def restore(self) -\u003e None\n```\n\n### AsyncTerminal\n\n```python\nclass AsyncTerminal:\n    async def __aenter__(self) -\u003e AsyncTerminal\n    async def __aexit__(self, ...) -\u003e bool\n    def draw(self, draw_fn: Callable[[Frame], None]) -\u003e None\n    async def poll_event(self, timeout_ms: int = 50) -\u003e KeyEvent | None\n    async def events(self, fps: float = 30.0, *, stop_on_quit: bool = False) -\u003e AsyncIterator[KeyEvent | None]\n    def area(self) -\u003e Rect\n    def clear(self) -\u003e None\n    def hide_cursor(self) -\u003e None\n    def show_cursor(self) -\u003e None\n```\n\n### Frame\n\n```python\nclass Frame:\n    @property\n    def area(self) -\u003e Rect\n\n    # Standard widgets (stateless)\n    def render_widget(self, widget: object, area: Rect) -\u003e None\n\n    # Stateful widgets\n    def render_stateful_list(self, widget: List, area: Rect, state: ListState) -\u003e None\n    def render_stateful_table(self, widget: Table, area: Rect, state: TableState) -\u003e None\n    def render_stateful_scrollbar(self, widget: Scrollbar, area: Rect, state: ScrollbarState) -\u003e None\n    def render_stateful_menu(self, widget: Menu, area: Rect, state: MenuState) -\u003e None\n\n    # Popups\n    def render_popup(self, popup: Popup, area: Rect) -\u003e None\n    def render_stateful_popup(self, popup: Popup, area: Rect, state: PopupState) -\u003e None\n\n    # Text editor\n    def render_textarea(self, ta: TextArea, area: Rect) -\u003e None\n\n    # Scroll view\n    def render_stateful_scrollview(self, sv: ScrollView, area: Rect, state: ScrollViewState) -\u003e None\n\n    # QR code\n    def render_qrcode(self, qr: QrCodeWidget, area: Rect) -\u003e None\n\n    # Effects\n    def apply_effect(self, effect: Effect, elapsed_ms: int, area: Rect) -\u003e None\n    def apply_effect_manager(self, manager: EffectManager, elapsed_ms: int, area: Rect) -\u003e None\n\n    # Prompts\n    def render_text_prompt(self, prompt: TextPrompt, area: Rect, state: TextState) -\u003e None\n    def render_password_prompt(self, prompt: PasswordPrompt, area: Rect, state: TextState) -\u003e None\n```\n\n### Layout \u0026 Geometry\n\n```python\nclass Layout:\n    def constraints(self, constraints: list[Constraint]) -\u003e Layout\n    def direction(self, direction: Direction) -\u003e Layout\n    def margin(self, margin: int) -\u003e Layout\n    def spacing(self, spacing: int) -\u003e Layout\n    def flex_mode(self, mode: str) -\u003e Layout\n    def split(self, area: Rect) -\u003e list[Rect]\n\nclass Rect:\n    x: int;  y: int;  width: int;  height: int\n    right: int;  bottom: int;  left: int;  top: int\n    def area(self) -\u003e int\n    def inner(self, horizontal: int = 1, vertical: int = 1) -\u003e Rect\n    def contains(self, other: Rect) -\u003e bool\n    def intersection(self, other: Rect) -\u003e Rect | None\n    def union(self, other: Rect) -\u003e Rect\n```\n\n### Style\n\n```python\nclass Style:\n    def fg(self, color: Color) -\u003e Style\n    def bg(self, color: Color) -\u003e Style\n    def bold(self) -\u003e Style\n    def italic(self) -\u003e Style\n    def underlined(self) -\u003e Style\n    def dim(self) -\u003e Style\n    def reversed(self) -\u003e Style\n    def hidden(self) -\u003e Style\n    def crossed_out(self) -\u003e Style\n    def slow_blink(self) -\u003e Style\n    def rapid_blink(self) -\u003e Style\n    def patch(self, other: Style) -\u003e Style\n    def add_modifier(self, modifier: Modifier) -\u003e Style\n    def remove_modifier(self, modifier: Modifier) -\u003e Style\n```\n\n### Block\n\n```python\nclass Block:\n    def title(self, title: str) -\u003e Block\n    def title_bottom(self, title: str) -\u003e Block\n    def bordered(self) -\u003e Block                          # all four borders\n    def borders(self, top, right, bottom, left) -\u003e Block\n    def border_type(self, bt: BorderType) -\u003e Block       # Plain | Rounded | Double | Thick\n    def style(self, style: Style) -\u003e Block\n    def border_style(self, style: Style) -\u003e Block\n    def title_style(self, style: Style) -\u003e Block\n    def padding(self, left, right, top, bottom) -\u003e Block\n    def title_alignment(self, alignment: str) -\u003e Block\n```\n\n### Prompts\n\n```python\nfrom pyratatui import (\n    Terminal,\n    TextPrompt,\n    TextState,\n    prompt_password,\n    prompt_text,\n)\n\n# Blocking single-line text prompt (runs its own event loop)\nvalue: str | None = prompt_text(\"Enter your name: \")\npassword: str | None = prompt_password(\"Password: \")\n\n# Stateful inline prompts\nstate = TextState()\nstate.focus()\n\nwith Terminal() as term:\n    term.hide_cursor()\n\n    while state.is_pending():\n        def ui(frame, _state=state):\n            frame.render_text_prompt(TextPrompt(\"Search: \"), frame.area, _state)\n\n        term.draw(ui)\n        ev = term.poll_event(timeout_ms=50)\n        if ev:\n            state.handle_key(ev)\n\n    term.show_cursor()\n\nif state.is_complete():\n    print(state.value())\nelif state.is_aborted():\n    print(\"Prompt aborted.\")\n```\n\n### Exceptions\n\n| Exception | When raised |\n|---|---|\n| `PyratatuiError` | Base exception for all library errors |\n| `BackendError` | Terminal backend failure |\n| `LayoutError` | Invalid layout constraint or split |\n| `RenderError` | Widget render failure |\n| `AsyncError` | Async / thread misuse |\n| `StyleError` | Invalid style combination |\n\n---\n\n## Examples\n\nThe `examples/` directory contains 38 standalone, runnable scripts. Run any of them directly:\n\n```bash\npython examples/01_hello_world.py\npython examples/07_async_reactive.py\npython examples/08_effects_fade.py\n```\n\nOR run all of them:\n\n```bash\npython test_all_examples.py\n```\n\n| # | File | Demonstrates |\n|---|---|---|\n| 01 | `01_hello_world.py` | `Terminal`, `Paragraph`, `Block`, `Style`, `Color` |\n| 02 | `02_layout.py` | `Layout`, `Constraint`, `Direction`, nested splits |\n| 03 | `03_styled_text.py` | `Span`, `Line`, `Text`, `Modifier` |\n| 04 | `04_list_navigation.py` | `List`, `ListState`, keyboard navigation |\n| 05 | `05_progress_bar.py` | `Gauge`, `LineGauge`, time-based animation |\n| 06 | `06_table_dynamic.py` | `Table`, `Row`, `Cell`, `TableState` |\n| 07 | `07_async_reactive.py` | `AsyncTerminal`, live background metrics |\n| 08 | `08_effects_fade.py` | `Effect.fade_from_fg`, `EffectManager` |\n| 09 | `09_effects_dsl.py` | `compile_effect()`, DSL syntax |\n| 10 | `10_full_app.py` | Full production app: tabs, async, effects |\n| 11 | `11_popup_basic.py` | `Popup` — basic centered popup |\n| 12 | `12_popup_stateful.py` | `PopupState` — draggable popup |\n| 13 | `13_popup_scrollable.py` | `KnownSizeWrapper` — scrollable popup content |\n| 14 | `14_textarea_basic.py` | `TextArea` — basic multi-line editor |\n| 15 | `15_textarea_advanced.py` | `TextArea` — modal vim-style editing |\n| 16 | `16_scrollview.py` | `ScrollView`, `ScrollViewState` |\n| 17 | `17_qrcode.py` | `QrCodeWidget`, `QrColors` |\n| 18 | `18_async_progress.py` | Async live progress with `asyncio.Task` |\n| 19 | `19_effects_glitch.py` | `dissolve` / `coalesce` glitch animation |\n| 20 | `20_effects_matrix.py` | `sweep_in` / `sweep_out` matrix-style |\n| 21 | `21_prompt_confirm.py` | Yes/No confirmation prompt |\n| 22 | `22_prompt_select.py` | Arrow-key selection menu |\n| 23 | `23_prompt_text.py` | `TextPrompt`, `TextState` |\n| 24 | `24_dashboard.py` | Full dashboard: `Tabs`, `BarChart`, `Sparkline` |\n| 25 | `25_calendar.py` | `Monthly`, `CalendarDate`, `CalendarEventStore` |\n| 26 | `26_bar_graph.py` | `BarGraph`, gradient styles |\n| 27 | `27_tree_widget.py` | `Tree`, `TreeState`, collapsible nodes |\n| 28 | `28_markdown_renderer.py` | `markdown_to_text()` |\n| 29 | `29_logger_demo.py` | `TuiLoggerWidget`, `init_logger` |\n| 30 | `30_image_view.py` | `ImagePicker`, `ImageWidget`, `ImageState` |\n| 31 | `31_canvas_drawing.py` | `Canvas` — lines, points, rectangles |\n| 32 | `32_map_widget.py` | `Map`, `MapResolution` |\n| 33 | `33_button_widget.py` | `Button` — focus state, key handling |\n| 34 | `34_throbber.py` | `Throbber` — start/stop and speed control |\n| 35 | `35_menu_widget.py` | `Menu`, `MenuState`, `MenuEvent` |\n| 36 | `36_piechart.py` | `PieChart`, `PieData`, `PieStyle` |\n| 37 | `37_checkbox_widget.py` | `Checkbox` — checked/unchecked toggle |\n| 38 | `38_chart_widget.py` | `Chart`, `Dataset`, `Axis`, `GraphType` |\n\n---\n\n## Building from Source\n\n### Prerequisites\n\n```bash\n# 1. Install Rust\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\nsource \"$HOME/.cargo/env\"\nrustup update stable\n\n# 2. Install Maturin\npip install maturin\n```\n\n### Development Build\n\n```bash\ngit clone https://github.com/pyratatui/pyratatui.git\ncd pyratatui\n\n# Editable install — fast compile, slower runtime\nmaturin develop\n\n# Release build — full Rust optimizations (recommended for benchmarking/use)\nmaturin develop --release\n```\n\nAfter changing Rust source files, re-run `maturin develop` to rebuild the extension. Python files in `python/pyratatui/` are reflected immediately with no rebuild.\n\n### Build a Distributable Wheel\n\n```bash\nmaturin build --release\n# Wheel output: target/wheels/pyratatui-*.whl\npip install target/wheels/pyratatui-*.whl\n```\n\n### Format \u0026 Lint\n\n```bash\n# Linux / macOS\n./scripts/format.sh\n\n# Windows\n./scripts/format.ps1\n\n# Python only (ruff + mypy)\nruff check .\nruff format .\nmypy python/\n```\n\n### Tests\n\n```bash\n# Python tests (pytest)\npytest tests/python/\n\n# Rust unit tests\ncargo test\n```\n\n### Docker (source build)\n\n```dockerfile\nFROM python:3.12-slim\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN pip install pyratatui\n```\n\n---\n\n## Platform Notes\n\n### Windows\n\nRequires **Windows Terminal** or **VS Code integrated terminal** (Windows 10 build 1903+ for VT sequence support). The classic `cmd.exe` may not render all Unicode characters correctly.\n\n### macOS\n\nDefault Terminal.app works but has limited colour support. [iTerm2](https://iterm2.com) or [Alacritty](https://alacritty.org) are recommended for true-colour and full Unicode rendering.\n\n### Linux\n\nAny modern terminal emulator works. Verify true-colour support:\n\n```bash\necho $COLORTERM   # should output \"truecolor\" or \"24bit\"\n```\n\n### Troubleshooting\n\n**`ModuleNotFoundError: No module named 'pyratatui._pyratatui'`**\nThe native extension was not compiled. Run `maturin develop --release` or reinstall via `pip install --force-reinstall pyratatui`.\n\n**`PanicException: pyratatui::terminal::Terminal is unsendable`**\nYou called a `Terminal` method from a thread-pool thread. Use `AsyncTerminal` instead.\n\n**Garbage on screen after Ctrl-C**\nAlways use `Terminal` as a context manager. For emergency recovery: `reset` or `stty sane` in your shell.\n\n**`ValueError: Invalid date`**\n`CalendarDate.from_ymd(y, m, d)` raises `ValueError` for invalid dates (e.g. Feb 30). Validate inputs first.\n\n---\n\n## Contributing\n\nContributions are welcome! Here's how to get started:\n\n1. **Fork** the repository on GitHub\n2. **Clone** your fork and create a branch: `git checkout -b feature/my-feature`\n3. **Install dev dependencies:**\n   ```bash\n   pip install -e \".[dev]\"\n   maturin develop\n   ```\n4. **Make your changes** — Rust source lives in `src/`, Python in `python/pyratatui/`\n5. **Run tests and linters:**\n   ```bash\n   pytest tests/python/\n   cargo test\n   ruff check . \u0026\u0026 ruff format .\n   mypy python/\n   ```\n6. **Open a Pull Request** against `main`\n\nPlease follow the existing code style. For significant changes, open an issue first to discuss your approach.\n\n### Documentation\n\nDocs are built with MkDocs Material:\n\n```bash\npip install -e \".[docs]\"\nmkdocs serve          # local preview at http://localhost:8000\nmkdocs build          # static site in site/\n```\n\n---\n\n## License\n\nMIT © 2026 PyRatatui contributors — see [LICENSE](LICENSE) for full text.\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\nBuilt with 🦀 [ratatui](https://ratatui.rs) · ⚡ [tachyonfx](https://github.com/junkdog/tachyonfx) · 🐍 [PyO3](https://pyo3.rs)\n\n[GitHub](https://github.com/pyratatui/pyratatui) · [PyPI](https://pypi.org/project/pyratatui/) · [Docs](https://pyratatui.github.io/pyratatui) · [Issues](https://github.com/pyratatui/pyratatui/issues)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyratatui%2Fpyratatui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpyratatui%2Fpyratatui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyratatui%2Fpyratatui/lists"}