Modern Text User Interface framework for building cross-platform terminal and web applications with Python
Overall
score
93%
CSS-like styling system with layout algorithms, reactive properties for automatic UI updates, color management, and geometric utilities for precise UI positioning and sizing.
Textual's CSS-like styling system allows you to style widgets using familiar CSS properties and selectors.
class Styles:
"""Container for CSS style properties."""
def __init__(self):
"""Initialize styles container."""
def __setattr__(self, name: str, value: Any) -> None:
"""Set a style property."""
def __getattr__(self, name: str) -> Any:
"""Get a style property value."""
def refresh(self) -> None:
"""Refresh the styles."""
# Common style properties (partial list)
width: int | str # Widget width
height: int | str # Widget height
min_width: int # Minimum width
min_height: int # Minimum height
max_width: int # Maximum width
max_height: int # Maximum height
margin: tuple[int, int, int, int] # Margin (top, right, bottom, left)
padding: tuple[int, int, int, int] # Padding
border: tuple[str, Color] # Border style and color
background: Color # Background color
color: Color # Text color
text_align: str # Text alignment ("left", "center", "right")
opacity: float # Transparency (0.0 to 1.0)
display: str # Display type ("block", "none")
visibility: str # Visibility ("visible", "hidden")
class StyleSheet:
"""CSS stylesheet management."""
def __init__(self):
"""Initialize stylesheet."""
def add_source(self, css: str, *, path: str | None = None) -> None:
"""
Add CSS source to the stylesheet.
Parameters:
- css: CSS text content
- path: Optional file path for debugging
"""
def parse(self, css: str) -> None:
"""Parse CSS text and add rules."""
def read(self, path: str | Path) -> None:
"""Read CSS from a file."""
class CSSPathError(Exception):
"""Raised when CSS path is invalid."""
class DeclarationError(Exception):
"""Raised when CSS declaration is invalid."""Reactive properties automatically update the UI when values change, similar to modern web frameworks.
class Reactive:
"""Reactive attribute descriptor for automatic UI updates."""
def __init__(
self,
default: Any = None,
*,
layout: bool = False,
repaint: bool = True,
init: bool = False,
always_update: bool = False,
compute: bool = True,
recompose: bool = False,
bindings: bool = False,
toggle_class: str | None = None
):
"""
Initialize reactive descriptor.
Parameters:
- default: Default value or callable that returns default
- layout: Whether changes trigger layout recalculation
- repaint: Whether changes trigger widget repaint
- init: Call watchers on initialization (post mount)
- always_update: Call watchers even when new value equals old
- compute: Run compute methods when attribute changes
- recompose: Compose the widget again when attribute changes
- bindings: Refresh bindings when reactive changes
- toggle_class: CSS class to toggle based on value truthiness
"""
def __get__(self, instance: Any, owner: type) -> Any:
"""Get the reactive value."""
def __set__(self, instance: Any, value: Any) -> None:
"""Set the reactive value and trigger updates."""
class ComputedProperty:
"""A computed reactive property."""
def __init__(self, function: Callable):
"""
Initialize computed property.
Parameters:
- function: Function to compute the value
"""
def watch(attribute: str):
"""
Decorator for watching reactive attribute changes.
Parameters:
- attribute: Name of reactive attribute to watch
Usage:
@watch("count")
def count_changed(self, old_value, new_value):
pass
"""Comprehensive color management with support for various color formats and ANSI terminal colors.
class Color:
"""RGB color with alpha channel and ANSI support."""
def __init__(self, r: int, g: int, b: int, a: float = 1.0):
"""
Initialize a color.
Parameters:
- r: Red component (0-255)
- g: Green component (0-255)
- b: Blue component (0-255)
- a: Alpha channel (0.0-1.0)
"""
@classmethod
def parse(cls, color_text: str) -> Color:
"""
Parse color from text.
Parameters:
- color_text: Color specification (hex, name, rgb(), etc.)
Returns:
Parsed Color instance
"""
@classmethod
def from_hsl(cls, h: float, s: float, l: float, a: float = 1.0) -> Color:
"""
Create color from HSL values.
Parameters:
- h: Hue (0.0-360.0)
- s: Saturation (0.0-1.0)
- l: Lightness (0.0-1.0)
- a: Alpha (0.0-1.0)
"""
def __str__(self) -> str:
"""Get color as CSS-style string."""
def with_alpha(self, alpha: float) -> Color:
"""
Create new color with different alpha.
Parameters:
- alpha: New alpha value
Returns:
New Color instance
"""
# Properties
r: int # Red component
g: int # Green component
b: int # Blue component
a: float # Alpha channel
hex: str # Hex representation
rgb: tuple[int, int, int] # RGB tuple
rgba: tuple[int, int, int, float] # RGBA tuple
class HSL:
"""HSL color representation."""
def __init__(self, h: float, s: float, l: float, a: float = 1.0):
"""
Initialize HSL color.
Parameters:
- h: Hue (0.0-360.0)
- s: Saturation (0.0-1.0)
- l: Lightness (0.0-1.0)
- a: Alpha (0.0-1.0)
"""
# Properties
h: float # Hue
s: float # Saturation
l: float # Lightness
a: float # Alpha
class Gradient:
"""Color gradient definition."""
def __init__(self, *stops: tuple[float, Color]):
"""
Initialize gradient with color stops.
Parameters:
- *stops: Color stops as (position, color) tuples
"""
def get_color(self, position: float) -> Color:
"""
Get interpolated color at position.
Parameters:
- position: Position in gradient (0.0-1.0)
Returns:
Interpolated color
"""
class ColorParseError(Exception):
"""Raised when color parsing fails."""Types and utilities for managing widget positioning, sizing, and layout calculations.
class Offset:
"""X,Y coordinate pair."""
def __init__(self, x: int, y: int):
"""
Initialize offset.
Parameters:
- x: Horizontal offset
- y: Vertical offset
"""
def __add__(self, other: Offset) -> Offset:
"""Add two offsets."""
def __sub__(self, other: Offset) -> Offset:
"""Subtract two offsets."""
# Properties
x: int # Horizontal coordinate
y: int # Vertical coordinate
class Size:
"""Width and height dimensions."""
def __init__(self, width: int, height: int):
"""
Initialize size.
Parameters:
- width: Width in characters/cells
- height: Height in characters/cells
"""
def __add__(self, other: Size) -> Size:
"""Add two sizes."""
def __sub__(self, other: Size) -> Size:
"""Subtract two sizes."""
@property
def area(self) -> int:
"""Get total area (width * height)."""
# Properties
width: int # Width dimension
height: int # Height dimension
class Region:
"""Rectangular area with offset and size."""
def __init__(self, x: int, y: int, width: int, height: int):
"""
Initialize region.
Parameters:
- x: Left edge X coordinate
- y: Top edge Y coordinate
- width: Region width
- height: Region height
"""
@classmethod
def from_corners(cls, x1: int, y1: int, x2: int, y2: int) -> Region:
"""
Create region from corner coordinates.
Parameters:
- x1, y1: Top-left corner
- x2, y2: Bottom-right corner
"""
def contains(self, x: int, y: int) -> bool:
"""
Check if point is within region.
Parameters:
- x: Point X coordinate
- y: Point Y coordinate
Returns:
True if point is inside region
"""
def intersect(self, other: Region) -> Region:
"""
Get intersection with another region.
Parameters:
- other: Region to intersect with
Returns:
Intersection region
"""
def union(self, other: Region) -> Region:
"""
Get union with another region.
Parameters:
- other: Region to union with
Returns:
Union region
"""
# Properties
x: int # Left edge
y: int # Top edge
width: int # Width
height: int # Height
size: Size # Size as Size object
offset: Offset # Offset as Offset object
area: int # Total area
class Spacing:
"""Padding/margin spacing values."""
def __init__(self, top: int, right: int, bottom: int, left: int):
"""
Initialize spacing.
Parameters:
- top: Top spacing
- right: Right spacing
- bottom: Bottom spacing
- left: Left spacing
"""
@classmethod
def all(cls, value: int) -> Spacing:
"""Create uniform spacing."""
@classmethod
def horizontal(cls, value: int) -> Spacing:
"""Create horizontal-only spacing."""
@classmethod
def vertical(cls, value: int) -> Spacing:
"""Create vertical-only spacing."""
# Properties
top: int
right: int
bottom: int
left: int
horizontal: int # Combined left + right
vertical: int # Combined top + bottom
def clamp(value: float, minimum: float, maximum: float) -> float:
"""
Clamp value within range.
Parameters:
- value: Value to clamp
- minimum: Minimum allowed value
- maximum: Maximum allowed value
Returns:
Clamped value
"""Layout algorithms for arranging widgets within containers.
class Layout:
"""Base class for layout algorithms."""
def __init__(self):
"""Initialize layout."""
def arrange(
self,
parent: Widget,
children: list[Widget],
size: Size
) -> dict[Widget, Region]:
"""
Arrange child widgets within parent.
Parameters:
- parent: Parent container widget
- children: List of child widgets to arrange
- size: Available size for arrangement
Returns:
Mapping of widgets to their regions
"""
class VerticalLayout(Layout):
"""Stack widgets vertically."""
pass
class HorizontalLayout(Layout):
"""Arrange widgets horizontally."""
pass
class GridLayout(Layout):
"""CSS Grid-like layout system."""
def __init__(self, *, columns: int = 1, rows: int = 1):
"""
Initialize grid layout.
Parameters:
- columns: Number of grid columns
- rows: Number of grid rows
"""from textual.app import App
from textual.widgets import Static
from textual.color import Color
class StyledApp(App):
# External CSS file
CSS_PATH = "app.css"
def compose(self):
yield Static("Styled content", classes="fancy-box")
def on_mount(self):
# Programmatic styling
static = self.query_one(Static)
static.styles.background = Color.parse("blue")
static.styles.color = Color.parse("white")
static.styles.padding = (1, 2)
static.styles.border = ("solid", Color.parse("yellow"))
# app.css content:
"""
.fancy-box {
width: 50%;
height: 10;
text-align: center;
margin: 2;
border: thick $primary;
background: $surface;
}
Static:hover {
background: $primary 50%;
}
"""from textual.app import App
from textual.widget import Widget
from textual.reactive import reactive, watch
from textual.widgets import Button, Static
class Counter(Widget):
"""A counter widget with reactive properties."""
# Reactive attribute that triggers repaint when changed
count = reactive(0)
def compose(self):
yield Static(f"Count: {self.count}", id="display")
yield Button("+", id="increment")
yield Button("-", id="decrement")
@watch("count")
def count_changed(self, old_value: int, new_value: int):
"""Called when count changes."""
display = self.query_one("#display", Static)
display.update(f"Count: {new_value}")
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "increment":
self.count += 1
elif event.button.id == "decrement":
self.count -= 1
class ReactiveApp(App):
def compose(self):
yield Counter()from textual.app import App
from textual.widgets import Static
from textual.color import Color, HSL, Gradient
class ColorApp(App):
def compose(self):
yield Static("Red text", id="red")
yield Static("HSL color", id="hsl")
yield Static("Parsed color", id="parsed")
def on_mount(self):
# Direct RGB color
red_widget = self.query_one("#red")
red_widget.styles.color = Color(255, 0, 0)
# HSL color conversion
hsl_widget = self.query_one("#hsl")
hsl_color = Color.from_hsl(240, 1.0, 0.5) # Blue
hsl_widget.styles.color = hsl_color
# Parse color from string
parsed_widget = self.query_one("#parsed")
parsed_widget.styles.color = Color.parse("#00ff00") # Green
# Gradient background (if supported)
gradient = Gradient(
(0.0, Color.parse("red")),
(0.5, Color.parse("yellow")),
(1.0, Color.parse("blue"))
)from textual.app import App
from textual.widget import Widget
from textual.geometry import Offset, Size, Region
from textual.events import MouseDown
class GeometryWidget(Widget):
"""Widget demonstrating geometric utilities."""
def __init__(self):
super().__init__()
self.center_region = Region(10, 5, 20, 10)
def on_mouse_down(self, event: MouseDown):
"""Handle mouse clicks with geometric calculations."""
click_point = Offset(event.x, event.y)
# Check if click is in center region
if self.center_region.contains(event.x, event.y):
self.log("Clicked in center region!")
# Calculate distance from center
center = Offset(
self.center_region.x + self.center_region.width // 2,
self.center_region.y + self.center_region.height // 2
)
distance_offset = click_point - center
distance = (distance_offset.x ** 2 + distance_offset.y ** 2) ** 0.5
self.log(f"Distance from center: {distance:.1f}")
class GeometryApp(App):
def compose(self):
yield GeometryWidget()from textual.app import App
from textual.containers import Container
from textual.widgets import Static
from textual.layouts import GridLayout
class LayoutApp(App):
def compose(self):
# Grid layout container
with Container():
container = self.query_one(Container)
container.styles.layout = GridLayout(columns=2, rows=2)
yield Static("Top Left", classes="grid-item")
yield Static("Top Right", classes="grid-item")
yield Static("Bottom Left", classes="grid-item")
yield Static("Bottom Right", classes="grid-item")
# CSS for grid items
"""
.grid-item {
height: 5;
border: solid white;
text-align: center;
content-align: center middle;
}
"""Install 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