CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-textual

Modern Text User Interface framework for building cross-platform terminal and web applications with Python

Overall
score

93%

Overview
Eval results
Files

content.mddocs/

Content and Rendering

Rich content system for displaying formatted text, custom renderables for specialized visualizations, content processing utilities, and DOM manipulation functions for widget tree traversal.

Capabilities

Rich Content System

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 object

Strip Rendering

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

Custom Renderables

Specialized 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."""

DOM Tree Traversal

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

Content Processing Utilities

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

Usage Examples

Rich Content Creation

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)

Custom Renderables

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)

DOM Tree Traversal

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}")

CSS Queries and DOM Manipulation

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

Strip-based Custom Widget

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

Install with Tessl CLI

npx tessl i tessl/pypi-textual

docs

content.md

core-framework.md

events.md

index.md

styling.md

testing.md

widgets.md

tile.json