Modern Text User Interface framework for building cross-platform terminal and web applications with Python
Overall
score
93%
Rich content system for displaying formatted text, custom renderables for specialized visualizations, content processing utilities, and DOM manipulation functions for widget tree traversal.
Textual's content system allows for rich text display with styling spans and formatting.
class Content:
"""Rich text content with styling spans."""
def __init__(self, text: str = "", spans: list[Span] | None = None):
"""
Initialize content.
Parameters:
- text: Plain text content
- spans: List of styling spans
"""
def __str__(self) -> str:
"""Get plain text representation."""
def __len__(self) -> int:
"""Get content length."""
def copy(self) -> Content:
"""Create a copy of the content."""
def append(self, text: str, *, style: Style | None = None) -> None:
"""
Append text with optional styling.
Parameters:
- text: Text to append
- style: Rich Style object for formatting
"""
def append_text(self, text: str) -> None:
"""Append plain text without styling."""
def extend(self, content: Content) -> None:
"""
Extend with another Content object.
Parameters:
- content: Content to append
"""
def split(self, separator: str = "\n") -> list[Content]:
"""
Split content by separator.
Parameters:
- separator: String to split on
Returns:
List of Content objects
"""
def truncate(self, max_length: int, *, overflow: str = "ellipsis") -> Content:
"""
Truncate content to maximum length.
Parameters:
- max_length: Maximum character length
- overflow: How to handle overflow ("ellipsis", "ignore")
Returns:
Truncated content
"""
# Properties
text: str # Plain text without styling
spans: list[Span] # List of styling spans
cell_length: int # Length in terminal cells
class Span:
"""Text styling span."""
def __init__(self, start: int, end: int, style: Style):
"""
Initialize a span.
Parameters:
- start: Start character index
- end: End character index
- style: Rich Style object
"""
def __contains__(self, index: int) -> bool:
"""Check if index is within span."""
# Properties
start: int # Start character index
end: int # End character index
style: Style # Rich Style objectStrip system for efficient line-by-line rendering in Textual.
class Strip:
"""Horizontal line of styled text cells."""
def __init__(self, segments: list[Segment] | None = None):
"""
Initialize strip.
Parameters:
- segments: List of styled text segments
"""
@classmethod
def blank(cls, length: int, *, style: Style | None = None) -> Strip:
"""
Create blank strip.
Parameters:
- length: Strip length in characters
- style: Optional styling
Returns:
Blank Strip instance
"""
def __len__(self) -> int:
"""Get strip length."""
def __bool__(self) -> bool:
"""Check if strip has content."""
def crop(self, start: int, end: int | None = None) -> Strip:
"""
Crop strip to range.
Parameters:
- start: Start index
- end: End index (None for end of strip)
Returns:
Cropped strip
"""
def extend(self, strips: Iterable[Strip]) -> Strip:
"""
Extend with other strips.
Parameters:
- strips: Strips to append
Returns:
Extended strip
"""
def apply_style(self, style: Style) -> Strip:
"""
Apply style to entire strip.
Parameters:
- style: Style to apply
Returns:
Styled strip
"""
# Properties
text: str # Plain text content
cell_length: int # Length in terminal cells
class Segment:
"""Text segment with styling."""
def __init__(self, text: str, style: Style | None = None):
"""
Initialize segment.
Parameters:
- text: Text content
- style: Optional Rich Style
"""
# Properties
text: str # Text content
style: Style | None # Styling informationSpecialized rendering components for charts, progress bars, and other visualizations.
class Bar:
"""Progress bar renderable."""
def __init__(
self,
size: Size,
*,
highlight_range: tuple[float, float] | None = None,
foreground_style: Style | None = None,
background_style: Style | None = None,
complete_style: Style | None = None,
):
"""
Initialize bar renderable.
Parameters:
- size: Bar dimensions
- highlight_range: Range to highlight (0.0-1.0)
- foreground_style: Foreground styling
- background_style: Background styling
- complete_style: Completed portion styling
"""
def __rich_console__(self, console, options) -> RenderResult:
"""Render the bar for Rich console."""
class Digits:
"""Digital display renderable for large numbers."""
def __init__(
self,
text: str,
*,
style: Style | None = None
):
"""
Initialize digits display.
Parameters:
- text: Text/numbers to display
- style: Display styling
"""
def __rich_console__(self, console, options) -> RenderResult:
"""Render digits for Rich console."""
class Sparkline:
"""Sparkline chart renderable."""
def __init__(
self,
data: Sequence[float],
*,
width: int | None = None,
min_color: Color | None = None,
max_color: Color | None = None,
summary_color: Color | None = None,
):
"""
Initialize sparkline.
Parameters:
- data: Numeric data points
- width: Chart width (None for auto)
- min_color: Color for minimum values
- max_color: Color for maximum values
- summary_color: Color for summary statistics
"""
def __rich_console__(self, console, options) -> RenderResult:
"""Render sparkline for Rich console."""
class Gradient:
"""Color gradient renderable."""
def __init__(
self,
size: Size,
stops: Sequence[tuple[float, Color]],
direction: str = "horizontal"
):
"""
Initialize gradient.
Parameters:
- size: Gradient dimensions
- stops: Color stops as (position, color) tuples
- direction: "horizontal" or "vertical"
"""
def __rich_console__(self, console, options) -> RenderResult:
"""Render gradient for Rich console."""
class Blank:
"""Empty space renderable."""
def __init__(self, width: int, height: int, *, style: Style | None = None):
"""
Initialize blank space.
Parameters:
- width: Width in characters
- height: Height in lines
- style: Optional background styling
"""
def __rich_console__(self, console, options) -> RenderResult:
"""Render blank space for Rich console."""Utilities for walking and querying the widget DOM tree.
def walk_children(
node: DOMNode,
*,
reverse: bool = False,
with_root: bool = True
) -> Iterator[DOMNode]:
"""
Walk immediate children of a DOM node.
Parameters:
- node: Starting DOM node
- reverse: Walk in reverse order
- with_root: Include the root node
Yields:
Child DOM nodes
"""
def walk_depth_first(
root: DOMNode,
*,
reverse: bool = False,
with_root: bool = True
) -> Iterator[DOMNode]:
"""
Walk DOM tree depth-first.
Parameters:
- root: Root node to start from
- reverse: Traverse in reverse order
- with_root: Include the root node
Yields:
DOM nodes in depth-first order
"""
def walk_breadth_first(
root: DOMNode,
*,
reverse: bool = False,
with_root: bool = True
) -> Iterator[DOMNode]:
"""
Walk DOM tree breadth-first.
Parameters:
- root: Root node to start from
- reverse: Traverse in reverse order
- with_root: Include the root node
Yields:
DOM nodes in breadth-first order
"""
class DOMNode:
"""Base DOM node with CSS query support."""
def query(self, selector: str) -> DOMQuery[DOMNode]:
"""
Query descendant nodes with CSS selector.
Parameters:
- selector: CSS selector string
Returns:
Query result set
"""
def query_one(
self,
selector: str,
expected_type: type[ExpectedType] = DOMNode
) -> ExpectedType:
"""
Query for single descendant node.
Parameters:
- selector: CSS selector string
- expected_type: Expected node type
Returns:
Single matching node
Raises:
NoMatches: If no nodes match
WrongType: If node is wrong type
TooManyMatches: If multiple nodes match
"""
def remove_class(self, *class_names: str) -> None:
"""
Remove CSS classes.
Parameters:
- *class_names: Class names to remove
"""
def add_class(self, *class_names: str) -> None:
"""
Add CSS classes.
Parameters:
- *class_names: Class names to add
"""
def toggle_class(self, *class_names: str) -> None:
"""
Toggle CSS classes.
Parameters:
- *class_names: Class names to toggle
"""
def has_class(self, class_name: str) -> bool:
"""
Check if node has CSS class.
Parameters:
- class_name: Class name to check
Returns:
True if class is present
"""
# Properties
id: str | None # Unique identifier
classes: set[str] # CSS classes
parent: DOMNode | None # Parent node
children: list[DOMNode] # Child nodes
ancestors: list[DOMNode] # All ancestor nodes
ancestors_with_self: list[DOMNode] # Ancestors including self
class DOMQuery:
"""Result set from DOM queries."""
def __init__(self, nodes: Iterable[DOMNode]):
"""
Initialize query result.
Parameters:
- nodes: Matching DOM nodes
"""
def __len__(self) -> int:
"""Get number of matching nodes."""
def __iter__(self) -> Iterator[DOMNode]:
"""Iterate over matching nodes."""
def __bool__(self) -> bool:
"""Check if query has results."""
def first(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:
"""
Get first matching node.
Parameters:
- expected_type: Expected node type
Returns:
First matching node
"""
def last(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:
"""
Get last matching node.
Parameters:
- expected_type: Expected node type
Returns:
Last matching node
"""
def remove(self) -> None:
"""Remove all matching nodes from DOM."""
def add_class(self, *class_names: str) -> None:
"""Add CSS classes to all matching nodes."""
def remove_class(self, *class_names: str) -> None:
"""Remove CSS classes from all matching nodes."""
def set_styles(self, **styles) -> None:
"""Set styles on all matching nodes."""
class NoMatches(Exception):
"""Raised when DOM query finds no matches."""
class WrongType(Exception):
"""Raised when DOM node is wrong type."""
class TooManyMatches(Exception):
"""Raised when DOM query finds too many matches."""def strip_links(content: Content) -> Content:
"""
Remove all links from content.
Parameters:
- content: Content to process
Returns:
Content with links removed
"""
def highlight_words(
content: Content,
words: Iterable[str],
*,
style: Style,
case_sensitive: bool = False
) -> Content:
"""
Highlight specific words in content.
Parameters:
- content: Content to process
- words: Words to highlight
- style: Highlight style
- case_sensitive: Whether matching is case sensitive
Returns:
Content with highlighted words
"""
def truncate_middle(
text: str,
length: int,
*,
ellipsis: str = "…"
) -> str:
"""
Truncate text in the middle.
Parameters:
- text: Text to truncate
- length: Target length
- ellipsis: Ellipsis character
Returns:
Truncated text
"""from textual.app import App
from textual.widgets import Static
from textual.content import Content, Span
from rich.style import Style
class ContentApp(App):
def compose(self):
# Create rich content with spans
content = Content("Hello, bold world!")
content.spans.append(
Span(7, 11, Style(bold=True, color="red"))
)
yield Static(content, id="rich-text")
# Alternative: build content incrementally
content2 = Content()
content2.append("Normal text ")
content2.append("highlighted", style=Style(bgcolor="yellow"))
content2.append(" and back to normal")
yield Static(content2, id="incremental")
def on_mount(self):
# Manipulate content after creation
static = self.query_one("#rich-text", Static)
current_content = static.renderable
# Truncate if too long
if len(current_content.text) > 20:
truncated = current_content.truncate(20)
static.update(truncated)from textual.app import App
from textual.widgets import Static
from textual.renderables import Sparkline, Digits, Bar
from textual.geometry import Size
class RenderableApp(App):
def compose(self):
# Sparkline chart
data = [1, 3, 2, 5, 4, 6, 3, 2, 4, 1]
sparkline = Sparkline(data, width=20)
yield Static(sparkline, id="chart")
# Digital display
digits = Digits("12:34")
yield Static(digits, id="clock")
# Progress bar
bar = Bar(Size(30, 1), highlight_range=(0.0, 0.7))
yield Static(bar, id="progress")
def update_displays(self):
"""Update the displays with new data."""
import time
# Update clock
current_time = time.strftime("%H:%M")
digits = Digits(current_time)
self.query_one("#clock", Static).update(digits)
# Update progress
import random
progress = random.random()
bar = Bar(Size(30, 1), highlight_range=(0.0, progress))
self.query_one("#progress", Static).update(bar)from textual.app import App
from textual.containers import Container, Horizontal
from textual.widgets import Button, Static
from textual.walk import walk_depth_first, walk_breadth_first
class TraversalApp(App):
def compose(self):
with Container(id="main"):
yield Static("Header", id="header")
with Horizontal(id="buttons"):
yield Button("Button 1", id="btn1")
yield Button("Button 2", id="btn2")
yield Static("Footer", id="footer")
def on_mount(self):
# Walk all widgets depth-first
main_container = self.query_one("#main")
self.log("Depth-first traversal:")
for widget in walk_depth_first(main_container):
self.log(f" {widget.__class__.__name__}: {widget.id}")
self.log("Breadth-first traversal:")
for widget in walk_breadth_first(main_container):
self.log(f" {widget.__class__.__name__}: {widget.id}")
def on_button_pressed(self, event: Button.Pressed):
# Find all buttons in the app
all_buttons = self.query("Button")
self.log(f"Found {len(all_buttons)} buttons")
# Find parent container of clicked button
parent = event.button.parent
while parent and not parent.id == "main":
parent = parent.parent
if parent:
self.log(f"Button is in container: {parent.id}")from textual.app import App
from textual.widgets import Button, Static
from textual.containers import Container
class QueryApp(App):
def compose(self):
with Container():
yield Static("Status: Ready", classes="status info")
yield Button("Success", classes="action success")
yield Button("Warning", classes="action warning")
yield Button("Error", classes="action error")
def on_button_pressed(self, event: Button.Pressed):
# Query by class
status = self.query_one(".status", Static)
# Update based on button type
if event.button.has_class("success"):
status.update("Status: Success!")
status.remove_class("info", "warning", "error")
status.add_class("success")
elif event.button.has_class("warning"):
status.update("Status: Warning!")
status.remove_class("info", "success", "error")
status.add_class("warning")
elif event.button.has_class("error"):
status.update("Status: Error!")
status.remove_class("info", "success", "warning")
status.add_class("error")
# Style all action buttons
action_buttons = self.query(".action")
action_buttons.set_styles(opacity=0.7)
# Highlight clicked button
event.button.styles.opacity = 1.0from textual.widget import Widget
from textual.strip import Strip
from textual.geometry import Size
from rich.segment import Segment
from rich.style import Style
class ProgressWidget(Widget):
"""Custom widget using Strip rendering."""
def __init__(self, progress: float = 0.0):
super().__init__()
self.progress = max(0.0, min(1.0, progress))
def render_line(self, y: int) -> Strip:
"""Render a single line using Strip."""
if y != 0: # Only render on first line
return Strip.blank(self.size.width)
# Calculate progress bar dimensions
width = self.size.width
filled_width = int(width * self.progress)
# Create segments for filled and empty portions
segments = []
if filled_width > 0:
segments.append(
Segment("█" * filled_width, Style(color="green"))
)
if filled_width < width:
segments.append(
Segment("░" * (width - filled_width), Style(color="gray"))
)
return Strip(segments)
def get_content_width(self, container: Size, viewport: Size) -> tuple[int, int]:
"""Get content width."""
return (20, 20) # Fixed width
def get_content_height(self, container: Size, viewport: Size, width: int) -> tuple[int, int]:
"""Get content height."""
return (1, 1) # Single lineInstall with Tessl CLI
npx tessl i tessl/pypi-textualevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10