Professional Python bindings for ratatui 0.30 β powered by Rust & PyO3
Build rich, high-performance terminal UIs in Python β with the full power of Rust under the hood.
Quickstart Β· Installation Β· Widgets Β· Effects Β· Examples Β· API Reference Β· Docs
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
PyRatatui exposes the entire ratatui Rust TUI library to Python via a thin, zero-overhead PyO3 extension module. You get:
- Pixel-perfect terminal rendering from ratatui's battle-tested Rust layout engine
- 35+ widgets out of the box: gauges, tables, trees, menus, charts, calendars, QR codes, images, markdown, and more
- TachyonFX animations β fade, sweep, glitch, dissolve, and composable effect pipelines
- Async-native β
AsyncTerminal+asynciointegration for live, reactive UIs - Full type stubs β every class and method ships with
.pyiannotations for IDE autocomplete - Cross-platform β Linux, macOS, and Windows (pre-built wheels on PyPI for all three)
- Installation
- Quickstart
- Core Concepts
- Widget Reference
- TachyonFX Effects
- Async & Reactive UIs
- CLI Tool
- API Reference
- Examples
- Building from Source
- Contributing
- License
pip install pyratatuiPre-built wheels are published to PyPI for:
- Linux x86_64 (manylinux2014)
- Linux x86_64 and aarch64 (musllinux_1_2) (starting from v0.2.3)
- macOS x86_64 (starting from v0.2.2) and arm64 (universal2)
- Windows x86_64
If no wheel exists for your platform, pip will automatically compile from source (requires Rust β see Building from Source).
python -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows PowerShell
pip install pyratatui| Requirement | Minimum | Notes |
|---|---|---|
| Python | 3.10 | 3.11+ recommended |
| OS | Linux, macOS, Windows | crossterm backend |
| Rust | 1.75 | source builds only |
import pyratatui
print(pyratatui.__version__) # "0.2.5"
print(pyratatui.__ratatui_version__) # "0.30"from pyratatui import Block, Color, Paragraph, Style, Terminal
with Terminal() as term:
while True:
def ui(frame):
frame.render_widget(
Paragraph.from_string("Hello, pyratatui! π Press q to quit.")
.block(Block().bordered().title("Hello World"))
.style(Style().fg(Color.cyan())),
frame.area,
)
term.draw(ui)
ev = term.poll_event(timeout_ms=100)
if ev and ev.code == "q":
breakOutput:
β Hello World βββββββββββββββββββββββββββββββββββββββββββββ
β Hello, pyratatui! π Press q to quit. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
pyratatui init my_app
cd my_app
pip install -r requirements.txt
python main.pyfrom pyratatui import Paragraph, run_app
def ui(frame):
frame.render_widget(
Paragraph.from_string("Hello! Press q to quit."),
frame.area,
)
run_app(ui)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).
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.
with Terminal() as term:
term.draw(lambda frame: ...) # pyratatui creates frame and passes it in
ev = term.poll_event(timeout_ms=50) # KeyEvent | NoneFrame holds the drawable area and all render methods for the current pass:
def ui(frame):
area = frame.area # Rect β full terminal size
frame.render_widget(widget, area)Layout divides a Rect into child regions using constraints:
from pyratatui import (
Block,
Constraint,
Direction,
Layout,
Paragraph,
run_app,
)
def ui(frame):
header, body, footer = (
Layout()
.direction(Direction.Vertical)
.constraints([
Constraint.length(3), # fixed 3 rows
Constraint.fill(1), # takes remaining space
Constraint.length(1), # fixed 1 row
])
.split(frame.area)
)
frame.render_widget(Block().bordered().title("Header"), header)
frame.render_widget(
Paragraph.from_string("Main content").block(Block().bordered().title("Body")),
body,
)
frame.render_widget(Paragraph.from_string("Press q to quit"), footer)
run_app(ui)Constraint types:
| Constraint | Description |
|---|---|
Constraint.length(n) |
Exactly n rows/columns |
Constraint.percentage(pct) |
pct% of available space |
Constraint.fill(n) |
Fill remaining space (proportionally weighted) |
Constraint.min(n) |
At least n rows/columns |
Constraint.max(n) |
At most n rows/columns |
Constraint.ratio(num, den) |
Fractional proportion |
All styling flows through Style, Color, and Modifier:
from pyratatui import Style, Color, Modifier
style = (
Style()
.fg(Color.cyan())
.bg(Color.rgb(30, 30, 46))
.bold()
.italic()
)
# Named colors
Color.red() Color.green() Color.yellow()
Color.blue() Color.magenta() Color.cyan()
Color.white() Color.gray() Color.dark_gray()
# Light variants: Color.light_red(), Color.light_green(), ...
# 256-color: Color.indexed(42)
# True-color: Color.rgb(255, 100, 0)Text is composed bottom-up: Span β Line β Text:
from pyratatui import Block, Color, Line, Paragraph, Span, Style, Text, run_app
def ui(frame):
text = Text([
Line([
Span("Status: ", Style().bold()),
Span("OK", Style().fg(Color.green())),
Span(" | 99.9%", Style().fg(Color.cyan())),
]),
Line.from_string("Plain text line"),
Line.from_string("Right-aligned").right_aligned(),
])
frame.render_widget(
Paragraph(text).block(Block().bordered().title("Text Hierarchy")),
frame.area,
)
run_app(ui)ev = term.poll_event(timeout_ms=100)
if ev:
print(ev.code) # "q", "Enter", "Up", "Down", "F1", etc.
print(ev.ctrl) # True if Ctrl held
print(ev.alt) # True if Alt held
print(ev.shift) # True if Shift held
# Common key codes
# Letters/digits: "a", "Z", "5"
# Special: "Enter", "Esc", "Backspace", "Tab", "BackTab"
# Arrows: "Up", "Down", "Left", "Right"
# Function: "F1" β¦ "F12"
# Ctrl+C: ev.code == "c" and ev.ctrlTip β Closure Capture: Always snapshot mutable state into default arguments to avoid late-binding issues in fast render loops:
count = state["count"] def ui(frame, _count=count): # β captured by value, not reference ...
| Widget | Description |
|---|---|
Paragraph |
Single or multi-line text, wrapping, scrolling |
Block |
Bordered container with title, padding, and style |
List + ListState |
Scrollable, selectable list |
Table + TableState |
Multi-column table with header and footer |
Gauge |
Filled progress bar |
LineGauge |
Single-line progress indicator |
BarChart |
Grouped vertical bar chart |
Sparkline |
Inline sparkline trend chart |
Scrollbar + ScrollbarState |
Attach scrollbars to any widget |
Tabs |
Tabbed navigation bar |
Clear |
Clears a rectangular area (use under popups) |
Runnable widget gallery:
from pyratatui import (
Block,
Color,
Constraint,
Direction,
Gauge,
Layout,
List,
ListItem,
ListState,
Row,
Sparkline,
Style,
Table,
TableState,
Tabs,
run_app,
)
list_state = ListState()
list_state.select(0)
table_state = TableState()
table_state.select(0)
def ui(frame, _list_state=list_state, _table_state=table_state):
rows = (
Layout()
.direction(Direction.Vertical)
.constraints([
Constraint.length(3),
Constraint.length(3),
Constraint.fill(1),
Constraint.length(5),
])
.split(frame.area)
)
middle = (
Layout()
.direction(Direction.Horizontal)
.constraints([Constraint.percentage(40), Constraint.fill(1)])
.split(rows[2])
)
frame.render_widget(
Tabs(["Overview", "Logs", "Config"])
.select(1)
.block(Block().bordered().title("Tabs"))
.highlight_style(Style().fg(Color.yellow()).bold()),
rows[0],
)
frame.render_widget(
Gauge()
.percent(75)
.label("CPU 75%")
.style(Style().fg(Color.green()))
.block(Block().bordered().title("Gauge")),
rows[1],
)
items = [ListItem(s) for s in ["Alpha", "Beta", "Gamma"]]
frame.render_stateful_list(
List(items)
.block(Block().bordered().title("List"))
.highlight_style(Style().fg(Color.yellow()).bold())
.highlight_symbol("βΆ "),
middle[0],
_list_state,
)
header = Row.from_strings(["Name", "Status", "Uptime"]).style(
Style().fg(Color.cyan()).bold()
)
table_rows = [
Row.from_strings(["nginx", "running", "14d"]),
Row.from_strings(["postgres", "running", "21d"]),
Row.from_strings(["redis", "degraded", "3h"]),
]
frame.render_stateful_table(
Table(
table_rows,
[Constraint.fill(1), Constraint.length(10), Constraint.length(8)],
header=header,
)
.block(Block().bordered().title("Table"))
.highlight_style(Style().fg(Color.yellow()).bold())
.highlight_symbol("βΆ "),
middle[1],
_table_state,
)
frame.render_widget(
Sparkline()
.data([10, 40, 20, 80, 55, 90])
.max(100)
.style(Style().fg(Color.cyan()))
.block(Block().bordered().title("Sparkline")),
rows[3],
)
run_app(ui)| Widget | Crate | Description |
|---|---|---|
Popup / PopupState |
tui-popup |
Centered or draggable popups |
TextArea |
tui-textarea |
Full multi-line editor (Emacs keybindings, undo/redo) |
ScrollView / ScrollViewState |
tui-scrollview |
Scrollable virtual viewport |
QrCodeWidget |
tui-qrcode |
QR codes rendered in Unicode block characters |
Monthly / CalendarDate |
ratatui widget-calendar |
Monthly calendar with event styling |
BarGraph |
tui-bar-graph |
Gradient braille/block bar graphs |
Tree / TreeState |
tui-tree-widget |
Collapsible tree view |
TuiLoggerWidget |
tui-logger |
Live scrolling log viewer |
ImageWidget / ImagePicker |
ratatui-image |
Terminal image rendering |
Canvas |
ratatui |
Low-level line/point/rect drawing |
Map |
ratatui |
World map widget |
Button |
built-in | Focus-aware interactive button |
Throbber |
throbber-widgets-tui |
Animated spinner/progress indicator |
Menu / MenuState |
tui-menu |
Nested dropdown menus with event handling |
PieChart / PieData / PieStyle |
tui-piechart |
Pie chart widget with legend and percentages |
Checkbox |
tui-checkbox |
Configurable checkbox widget |
Chart / Dataset / Axis |
ratatui |
Multi-dataset cartesian chart (line/scatter/bar) |
Third-party widget gallery:
from pyratatui import (
BarColorMode,
BarGraph,
BarGraphStyle,
Block,
CalendarDate,
CalendarEventStore,
Color,
Constraint,
Direction,
Layout,
Monthly,
Paragraph,
Popup,
PopupState,
QrCodeWidget,
QrColors,
Style,
TextArea,
Tree,
TreeItem,
TreeState,
markdown_to_text,
run_app,
)
popup = Popup("Press q to dismiss").title("Popup").style(Style().bg(Color.blue()))
popup_state = PopupState()
textarea = TextArea.from_lines(["Hello", "World"])
textarea.set_block(Block().bordered().title("TextArea"))
tree = Tree([
TreeItem("src", [TreeItem("main.rs"), TreeItem("lib.rs")]),
TreeItem("Cargo.toml"),
]).block(Block().bordered().title("Tree"))
tree_state = TreeState()
tree_state.select([0])
def ui(frame, _popup_state=popup_state, _ta=textarea, _tree=tree, _tree_state=tree_state):
rows = (
Layout()
.direction(Direction.Vertical)
.constraints([
Constraint.length(12),
Constraint.length(10),
Constraint.fill(1),
])
.split(frame.area)
)
top = (
Layout()
.direction(Direction.Horizontal)
.constraints([
Constraint.percentage(25),
Constraint.percentage(25),
Constraint.percentage(25),
Constraint.fill(1),
])
.split(rows[0])
)
middle = (
Layout()
.direction(Direction.Horizontal)
.constraints([Constraint.fill(1), Constraint.length(28)])
.split(rows[1])
)
qr_block = Block().bordered().title("QR Code")
frame.render_widget(qr_block, top[0])
frame.render_qrcode(
QrCodeWidget("https://ratatui.rs").colors(QrColors.Inverted),
qr_block.inner(top[0]),
)
store = CalendarEventStore.today_highlighted(Style().fg(Color.green()).bold())
frame.render_widget(
Monthly(CalendarDate.today(), store)
.block(Block().bordered().title("Calendar"))
.show_month_header(Style().bold())
.show_weekdays_header(Style().italic()),
top[1],
)
graph_block = Block().bordered().title("Bar Graph")
frame.render_widget(graph_block, top[2])
frame.render_widget(
BarGraph([0.1, 0.4, 0.9, 0.6, 0.8])
.bar_style(BarGraphStyle.Braille)
.color_mode(BarColorMode.VerticalGradient)
.gradient("turbo"),
graph_block.inner(top[2]),
)
frame.render_stateful_popup(popup, top[3], _popup_state)
frame.render_widget(
Paragraph(markdown_to_text("# Hello\n\n**bold** _italic_ `code`"))
.block(Block().bordered().title("Markdown")),
middle[0],
)
frame.render_stateful_tree(_tree, middle[1], _tree_state)
frame.render_textarea(_ta, rows[2])
run_app(ui)PyRatatui ships the full tachyonfx effects engine. Effects are post-render transforms that mutate the frame buffer β always apply them after rendering your widgets.
| Effect | Description |
|---|---|
Effect.fade_from_fg(color, ms) |
Fade text from a color into its natural color |
Effect.fade_to_fg(color, ms) |
Fade text out to a flat color |
Effect.fade_from(bg, fg, ms) |
Fade both background and foreground from color |
Effect.fade_to(bg, fg, ms) |
Fade both background and foreground to color |
Effect.coalesce(ms) |
Characters materialize in from random positions |
Effect.dissolve(ms) |
Characters scatter and dissolve |
Effect.slide_in(direction, ms) |
Slide content in from an edge |
Effect.slide_out(direction, ms) |
Slide content out to an edge |
Effect.sweep_in(dir, span, grad, color, ms) |
Gradient sweep reveal |
Effect.sweep_out(dir, span, grad, color, ms) |
Gradient sweep hide |
Effect.sequence(effects) |
Run effects one after another |
Effect.parallel(effects) |
Run effects simultaneously |
Effect.sleep(ms) |
Delay before next effect in a sequence |
Effect.repeat(effect, times=-1) |
Loop an effect (β1 = forever) |
Effect.ping_pong(effect) |
Play an effect forward then backward |
Effect.never_complete(effect) |
Keep an effect alive indefinitely |
Interpolation.Linear, QuadIn/Out/InOut, CubicIn/Out/InOut, SineIn/Out/InOut,
CircIn/Out/InOut, ExpoIn/Out/InOut, ElasticIn/Out, BounceIn/Out/BounceInOut, BackIn/Out/BackInOut
import time
from pyratatui import Effect, EffectManager, Interpolation, Color, Terminal, Paragraph
mgr = EffectManager()
mgr.add(Effect.fade_from_fg(Color.black(), 1000, Interpolation.SineOut))
last = time.monotonic()
with Terminal() as term:
while not (ev := term.poll_event(timeout_ms=16)) or ev.code != "q":
now = time.monotonic()
elapsed_ms = int((now - last) * 1000)
last = now
def ui(frame, _mgr=mgr, _ms=elapsed_ms):
# Step 1 β render widgets
frame.render_widget(Paragraph.from_string("Fading inβ¦"), frame.area)
# Step 2 β apply effects to the same buffer
frame.apply_effect_manager(_mgr, _ms, frame.area)
term.draw(ui)Compile tachyonfx expressions at runtime β perfect for config-driven or user-customisable animations:
from pyratatui import compile_effect, EffectManager
# DSL mirrors the Rust / tachyonfx expression syntax
effect = compile_effect("fx::coalesce(500)")
effect = compile_effect("fx::dissolve((800, BounceOut))")
effect = compile_effect("fx::fade_from_fg(Color::Black, (600, QuadOut))")
effect = compile_effect("fx::sweep_in(LeftToRight, 10, 5, Color::Black, (700, SineOut))")
mgr = EffectManager()
mgr.add(effect)Target effects at specific cells:
from pyratatui import CellFilter, Effect, Color
effect = Effect.fade_from_fg(Color.black(), 800)
effect.with_filter(CellFilter.text()) # text cells only
effect.with_filter(CellFilter.inner(horizontal=1, vertical=1)) # inner area
effect.with_filter(CellFilter.fg_color(Color.cyan())) # specific fg color
effect.with_filter(CellFilter.any_of([CellFilter.text(), CellFilter.all()]))Use AsyncTerminal to combine rendering with background asyncio tasks:
import asyncio
from pyratatui import AsyncTerminal, Gauge, Block, Style, Color
state = {"progress": 0}
async def background_worker():
while state["progress"] < 100:
await asyncio.sleep(0.1)
state["progress"] += 2
async def main():
worker = asyncio.create_task(background_worker())
async with AsyncTerminal() as term:
async for ev in term.events(fps=30):
pct = state["progress"]
def ui(frame, _pct=pct):
frame.render_widget(
Gauge()
.percent(_pct)
.label(f"Loading⦠{_pct}%")
.style(Style().fg(Color.green()))
.block(Block().bordered().title("Progress")),
frame.area,
)
term.draw(ui)
if ev and ev.code == "q":
break
if pct >= 100:
break
worker.cancel()
asyncio.run(main())By default events() keeps yielding each tick; pass stop_on_quit=True to opt into automatic exit on q/Ctrl+C.
async for ev in term.events(fps=30.0, stop_on_quit=True):
# ev is KeyEvent | None
# None emitted each tick (use for animations / periodic updates)
# stop_on_quit=True (opt-in) exits the loop automatically on "q" or Ctrl+CFor simpler apps that don't need manual task management; keep in mind that quitting must be implemented via on_key or another explicit signal.
from pyratatui import run_app, run_app_async, Paragraph
# Synchronous
def ui(frame):
frame.render_widget(
Paragraph.from_string("Hello!"),
frame.area
)
run_app(ui, on_key=lambda ev: ev.code == "q")
# Asynchronous
import asyncio
async def main():
tick = 0
def ui(frame):
nonlocal tick
frame.render_widget(Paragraph.from_string(f"Tick: {tick}"), frame.area)
tick += 1
await run_app_async(ui, fps=30, on_key=lambda ev: ev.code == "q")
asyncio.run(main())PyRatatui ships a pyratatui CLI for project scaffolding and version inspection.
Usage: pyratatui [COMMAND]
Commands:
init Create a new PyRatatui project scaffold
version Show PyRatatui version
Options:
--help Show help message
pyratatui init my_tui_app [--verbose]Creates a ready-to-run project:
my_tui_app/
βββ main.py # runnable hello world starter
βββ requirements.txt # pyratatui dependency
βββ README.md # project docs
cd my_tui_app
pip install -r requirements.txt
python main.pypyratatui version
# PyRatatui 0.2.5class Terminal:
def __enter__(self) -> Terminal
def __exit__(self, ...) -> bool
def draw(self, draw_fn: Callable[[Frame], None]) -> None
def poll_event(self, timeout_ms: int = 0) -> KeyEvent | None
def area(self) -> Rect
def clear(self) -> None
def hide_cursor(self) -> None
def show_cursor(self) -> None
def restore(self) -> Noneclass AsyncTerminal:
async def __aenter__(self) -> AsyncTerminal
async def __aexit__(self, ...) -> bool
def draw(self, draw_fn: Callable[[Frame], None]) -> None
async def poll_event(self, timeout_ms: int = 50) -> KeyEvent | None
async def events(self, fps: float = 30.0, *, stop_on_quit: bool = False) -> AsyncIterator[KeyEvent | None]
def area(self) -> Rect
def clear(self) -> None
def hide_cursor(self) -> None
def show_cursor(self) -> Noneclass Frame:
@property
def area(self) -> Rect
# Standard widgets (stateless)
def render_widget(self, widget: object, area: Rect) -> None
# Stateful widgets
def render_stateful_list(self, widget: List, area: Rect, state: ListState) -> None
def render_stateful_table(self, widget: Table, area: Rect, state: TableState) -> None
def render_stateful_scrollbar(self, widget: Scrollbar, area: Rect, state: ScrollbarState) -> None
def render_stateful_menu(self, widget: Menu, area: Rect, state: MenuState) -> None
# Popups
def render_popup(self, popup: Popup, area: Rect) -> None
def render_stateful_popup(self, popup: Popup, area: Rect, state: PopupState) -> None
# Text editor
def render_textarea(self, ta: TextArea, area: Rect) -> None
# Scroll view
def render_stateful_scrollview(self, sv: ScrollView, area: Rect, state: ScrollViewState) -> None
# QR code
def render_qrcode(self, qr: QrCodeWidget, area: Rect) -> None
# Effects
def apply_effect(self, effect: Effect, elapsed_ms: int, area: Rect) -> None
def apply_effect_manager(self, manager: EffectManager, elapsed_ms: int, area: Rect) -> None
# Prompts
def render_text_prompt(self, prompt: TextPrompt, area: Rect, state: TextState) -> None
def render_password_prompt(self, prompt: PasswordPrompt, area: Rect, state: TextState) -> Noneclass Layout:
def constraints(self, constraints: list[Constraint]) -> Layout
def direction(self, direction: Direction) -> Layout
def margin(self, margin: int) -> Layout
def spacing(self, spacing: int) -> Layout
def flex_mode(self, mode: str) -> Layout
def split(self, area: Rect) -> list[Rect]
class Rect:
x: int; y: int; width: int; height: int
right: int; bottom: int; left: int; top: int
def area(self) -> int
def inner(self, horizontal: int = 1, vertical: int = 1) -> Rect
def contains(self, other: Rect) -> bool
def intersection(self, other: Rect) -> Rect | None
def union(self, other: Rect) -> Rectclass Style:
def fg(self, color: Color) -> Style
def bg(self, color: Color) -> Style
def bold(self) -> Style
def italic(self) -> Style
def underlined(self) -> Style
def dim(self) -> Style
def reversed(self) -> Style
def hidden(self) -> Style
def crossed_out(self) -> Style
def slow_blink(self) -> Style
def rapid_blink(self) -> Style
def patch(self, other: Style) -> Style
def add_modifier(self, modifier: Modifier) -> Style
def remove_modifier(self, modifier: Modifier) -> Styleclass Block:
def title(self, title: str) -> Block
def title_bottom(self, title: str) -> Block
def bordered(self) -> Block # all four borders
def borders(self, top, right, bottom, left) -> Block
def border_type(self, bt: BorderType) -> Block # Plain | Rounded | Double | Thick
def style(self, style: Style) -> Block
def border_style(self, style: Style) -> Block
def title_style(self, style: Style) -> Block
def padding(self, left, right, top, bottom) -> Block
def title_alignment(self, alignment: str) -> Blockfrom pyratatui import (
Terminal,
TextPrompt,
TextState,
prompt_password,
prompt_text,
)
# Blocking single-line text prompt (runs its own event loop)
value: str | None = prompt_text("Enter your name: ")
password: str | None = prompt_password("Password: ")
# Stateful inline prompts
state = TextState()
state.focus()
with Terminal() as term:
term.hide_cursor()
while state.is_pending():
def ui(frame, _state=state):
frame.render_text_prompt(TextPrompt("Search: "), frame.area, _state)
term.draw(ui)
ev = term.poll_event(timeout_ms=50)
if ev:
state.handle_key(ev)
term.show_cursor()
if state.is_complete():
print(state.value())
elif state.is_aborted():
print("Prompt aborted.")| Exception | When raised |
|---|---|
PyratatuiError |
Base exception for all library errors |
BackendError |
Terminal backend failure |
LayoutError |
Invalid layout constraint or split |
RenderError |
Widget render failure |
AsyncError |
Async / thread misuse |
StyleError |
Invalid style combination |
The examples/ directory contains 38 standalone, runnable scripts. Run any of them directly:
python examples/01_hello_world.py
python examples/07_async_reactive.py
python examples/08_effects_fade.pyOR run all of them:
python test_all_examples.py| # | File | Demonstrates |
|---|---|---|
| 01 | 01_hello_world.py |
Terminal, Paragraph, Block, Style, Color |
| 02 | 02_layout.py |
Layout, Constraint, Direction, nested splits |
| 03 | 03_styled_text.py |
Span, Line, Text, Modifier |
| 04 | 04_list_navigation.py |
List, ListState, keyboard navigation |
| 05 | 05_progress_bar.py |
Gauge, LineGauge, time-based animation |
| 06 | 06_table_dynamic.py |
Table, Row, Cell, TableState |
| 07 | 07_async_reactive.py |
AsyncTerminal, live background metrics |
| 08 | 08_effects_fade.py |
Effect.fade_from_fg, EffectManager |
| 09 | 09_effects_dsl.py |
compile_effect(), DSL syntax |
| 10 | 10_full_app.py |
Full production app: tabs, async, effects |
| 11 | 11_popup_basic.py |
Popup β basic centered popup |
| 12 | 12_popup_stateful.py |
PopupState β draggable popup |
| 13 | 13_popup_scrollable.py |
KnownSizeWrapper β scrollable popup content |
| 14 | 14_textarea_basic.py |
TextArea β basic multi-line editor |
| 15 | 15_textarea_advanced.py |
TextArea β modal vim-style editing |
| 16 | 16_scrollview.py |
ScrollView, ScrollViewState |
| 17 | 17_qrcode.py |
QrCodeWidget, QrColors |
| 18 | 18_async_progress.py |
Async live progress with asyncio.Task |
| 19 | 19_effects_glitch.py |
dissolve / coalesce glitch animation |
| 20 | 20_effects_matrix.py |
sweep_in / sweep_out matrix-style |
| 21 | 21_prompt_confirm.py |
Yes/No confirmation prompt |
| 22 | 22_prompt_select.py |
Arrow-key selection menu |
| 23 | 23_prompt_text.py |
TextPrompt, TextState |
| 24 | 24_dashboard.py |
Full dashboard: Tabs, BarChart, Sparkline |
| 25 | 25_calendar.py |
Monthly, CalendarDate, CalendarEventStore |
| 26 | 26_bar_graph.py |
BarGraph, gradient styles |
| 27 | 27_tree_widget.py |
Tree, TreeState, collapsible nodes |
| 28 | 28_markdown_renderer.py |
markdown_to_text() |
| 29 | 29_logger_demo.py |
TuiLoggerWidget, init_logger |
| 30 | 30_image_view.py |
ImagePicker, ImageWidget, ImageState |
| 31 | 31_canvas_drawing.py |
Canvas β lines, points, rectangles |
| 32 | 32_map_widget.py |
Map, MapResolution |
| 33 | 33_button_widget.py |
Button β focus state, key handling |
| 34 | 34_throbber.py |
Throbber β start/stop and speed control |
| 35 | 35_menu_widget.py |
Menu, MenuState, MenuEvent |
| 36 | 36_piechart.py |
PieChart, PieData, PieStyle |
| 37 | 37_checkbox_widget.py |
Checkbox β checked/unchecked toggle |
| 38 | 38_chart_widget.py |
Chart, Dataset, Axis, GraphType |
# 1. Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustup update stable
# 2. Install Maturin
pip install maturingit clone https://github.com/pyratatui/pyratatui.git
cd pyratatui
# Editable install β fast compile, slower runtime
maturin develop
# Release build β full Rust optimizations (recommended for benchmarking/use)
maturin develop --releaseAfter changing Rust source files, re-run maturin develop to rebuild the extension. Python files in python/pyratatui/ are reflected immediately with no rebuild.
maturin build --release
# Wheel output: target/wheels/pyratatui-*.whl
pip install target/wheels/pyratatui-*.whl# Linux / macOS
./scripts/format.sh
# Windows
./scripts/format.ps1
# Python only (ruff + mypy)
ruff check .
ruff format .
mypy python/# Python tests (pytest)
pytest tests/python/
# Rust unit tests
cargo testFROM python:3.12-slim
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN pip install pyratatuiRequires 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.
Default Terminal.app works but has limited colour support. iTerm2 or Alacritty are recommended for true-colour and full Unicode rendering.
Any modern terminal emulator works. Verify true-colour support:
echo $COLORTERM # should output "truecolor" or "24bit"ModuleNotFoundError: No module named 'pyratatui._pyratatui'
The native extension was not compiled. Run maturin develop --release or reinstall via pip install --force-reinstall pyratatui.
PanicException: pyratatui::terminal::Terminal is unsendable
You called a Terminal method from a thread-pool thread. Use AsyncTerminal instead.
Garbage on screen after Ctrl-C
Always use Terminal as a context manager. For emergency recovery: reset or stty sane in your shell.
ValueError: Invalid date
CalendarDate.from_ymd(y, m, d) raises ValueError for invalid dates (e.g. Feb 30). Validate inputs first.
Contributions are welcome! Here's how to get started:
- Fork the repository on GitHub
- Clone your fork and create a branch:
git checkout -b feature/my-feature - Install dev dependencies:
pip install -e ".[dev]" maturin develop - Make your changes β Rust source lives in
src/, Python inpython/pyratatui/ - Run tests and linters:
pytest tests/python/ cargo test ruff check . && ruff format . mypy python/
- Open a Pull Request against
main
Please follow the existing code style. For significant changes, open an issue first to discuss your approach.
Docs are built with MkDocs Material:
pip install -e ".[docs]"
mkdocs serve # local preview at http://localhost:8000
mkdocs build # static site in site/MIT Β© 2026 PyRatatui contributors β see LICENSE for full text.

















