Modern Text User Interface framework for building cross-platform terminal and web applications with Python
Overall
score
93%
Programmatic testing framework with the Pilot class for automated UI testing, command palette system for built-in development tools, logging utilities for debugging applications, and background task management.
The Pilot class enables automated testing of Textual applications by simulating user interactions and verifying application state.
class Pilot:
"""Programmatic controller for testing Textual applications."""
def __init__(self, app: App):
"""
Initialize pilot for an application.
Parameters:
- app: Textual application to control
"""
async def press(self, *keys: str) -> None:
"""
Simulate key presses.
Parameters:
- *keys: Key names to press (e.g., "enter", "ctrl+c", "a")
"""
async def click(
self,
selector: str | None = None,
*,
offset: tuple[int, int] = (0, 0)
) -> None:
"""
Simulate mouse click.
Parameters:
- selector: CSS selector for target widget (None for current focus)
- offset: X,Y offset from widget origin
"""
async def hover(
self,
selector: str | None = None,
*,
offset: tuple[int, int] = (0, 0)
) -> None:
"""
Simulate mouse hover.
Parameters:
- selector: CSS selector for target widget
- offset: X,Y offset from widget origin
"""
async def scroll_down(self, selector: str | None = None) -> None:
"""
Simulate scroll down.
Parameters:
- selector: CSS selector for target widget
"""
async def scroll_up(self, selector: str | None = None) -> None:
"""
Simulate scroll up.
Parameters:
- selector: CSS selector for target widget
"""
async def wait_for_screen(self, screen: type[Screen] | str | None = None) -> None:
"""
Wait for a specific screen to be active.
Parameters:
- screen: Screen class, name, or None for any screen change
"""
async def wait_for_animation(self) -> None:
"""Wait for all animations to complete."""
async def pause(self, delay: float = 0.1) -> None:
"""
Pause execution.
Parameters:
- delay: Pause duration in seconds
"""
async def exit(self, result: Any = None) -> None:
"""
Exit the application.
Parameters:
- result: Exit result value
"""
# Properties
app: App # The controlled application
class OutOfBounds(Exception):
"""Raised when pilot operation is out of bounds."""
class WaitForScreenTimeout(Exception):
"""Raised when waiting for screen times out."""Built-in command palette for development tools and application commands.
class CommandPalette(Widget):
"""Built-in command palette widget."""
class Opened(Message):
"""Sent when command palette opens."""
class Closed(Message):
"""Sent when command palette closes."""
class OptionSelected(Message):
"""Sent when command is selected."""
def __init__(self, option: Hit): ...
def __init__(self, **kwargs):
"""Initialize command palette."""
def search(self, query: str) -> None:
"""
Search for commands.
Parameters:
- query: Search query string
"""
class Provider:
"""Base class for command providers."""
async def search(self, query: str) -> Iterable[Hit]:
"""
Search for commands matching query.
Parameters:
- query: Search query string
Returns:
Iterable of matching command hits
"""
async def discover(self) -> Iterable[Hit]:
"""
Discover all available commands.
Returns:
Iterable of all command hits
"""
class Hit:
"""Command search result."""
def __init__(
self,
match_score: int,
renderable: RenderableType,
*,
command: Callable | None = None,
help_text: str | None = None,
id: str | None = None
):
"""
Initialize command hit.
Parameters:
- match_score: Relevance score for search
- renderable: Display representation
- command: Callable to execute
- help_text: Help description
- id: Unique identifier
"""
# Properties
match_score: int
renderable: RenderableType
command: Callable | None
help_text: str | None
id: str | None
CommandList = list[Hit]
"""Type alias for list of command hits."""Comprehensive logging utilities for debugging and development.
def log(*args, **kwargs) -> None:
"""
Global logging function.
Parameters:
- *args: Values to log
- **kwargs: Keyword arguments to log
Note:
Logs to the currently active app or debug output if no app.
"""
class Logger:
"""Logger class that logs to the Textual console."""
def __init__(
self,
log_callable: LogCallable | None,
group: LogGroup = LogGroup.INFO,
verbosity: LogVerbosity = LogVerbosity.NORMAL,
app: App | None = None,
):
"""
Initialize logger.
Parameters:
- log_callable: Function to handle log calls
- group: Log group classification
- verbosity: Logging verbosity level
- app: Associated application
"""
def __call__(self, *args: object, **kwargs) -> None:
"""Log values and keyword arguments."""
def verbosity(self, verbose: bool) -> Logger:
"""
Get logger with different verbosity.
Parameters:
- verbose: True for high verbosity, False for normal
Returns:
New logger instance
"""
# Property loggers for different categories
@property
def verbose(self) -> Logger:
"""High verbosity logger."""
@property
def event(self) -> Logger:
"""Event logging."""
@property
def debug(self) -> Logger:
"""Debug message logging."""
@property
def info(self) -> Logger:
"""Information logging."""
@property
def warning(self) -> Logger:
"""Warning message logging."""
@property
def error(self) -> Logger:
"""Error message logging."""
@property
def system(self) -> Logger:
"""System information logging."""
@property
def logging(self) -> Logger:
"""Standard library logging integration."""
@property
def worker(self) -> Logger:
"""Worker/background task logging."""
class LogGroup(Enum):
"""Log message categories."""
INFO = "INFO"
DEBUG = "DEBUG"
WARNING = "WARNING"
ERROR = "ERROR"
EVENT = "EVENT"
SYSTEM = "SYSTEM"
LOGGING = "LOGGING"
WORKER = "WORKER"
class LogVerbosity(Enum):
"""Log verbosity levels."""
NORMAL = "NORMAL"
HIGH = "HIGH"
class LoggerError(Exception):
"""Raised when logger operation fails."""System for managing background tasks and workers in Textual applications.
class Worker:
"""Background worker for running tasks."""
def __init__(
self,
target: Callable,
*,
name: str | None = None,
group: str = "default",
description: str = "",
exit_on_error: bool = True,
start: bool = True
):
"""
Initialize worker.
Parameters:
- target: Function/coroutine to run
- name: Worker name
- group: Worker group name
- description: Worker description
- exit_on_error: Whether to exit app on worker error
- start: Whether to start immediately
"""
def start(self) -> None:
"""Start the worker."""
def cancel(self) -> None:
"""Cancel the worker."""
async def wait(self) -> Any:
"""Wait for worker completion and return result."""
# Properties
is_cancelled: bool # Whether worker is cancelled
is_finished: bool # Whether worker completed
state: WorkerState # Current worker state
result: Any # Worker result (if completed)
error: Exception | None # Worker error (if failed)
class WorkerState(Enum):
"""Worker execution states."""
PENDING = "PENDING"
RUNNING = "RUNNING"
CANCELLED = "CANCELLED"
ERROR = "ERROR"
SUCCESS = "SUCCESS"
class WorkerError(Exception):
"""Base worker error."""
class WorkerFailed(WorkerError):
"""Raised when worker fails."""
def work(exclusive: bool = False, thread: bool = False):
"""
Decorator for creating background workers.
Parameters:
- exclusive: Cancel other workers in same group when starting
- thread: Run in separate thread instead of async task
Usage:
@work
async def background_task(self):
# Background work here
pass
"""Scheduling and timing utilities for delayed execution and periodic tasks.
class Timer:
"""Scheduled callback timer."""
def __init__(
self,
name: str,
interval: float,
callback: TimerCallback,
*,
repeat: int | None = None,
pause: bool = False
):
"""
Initialize timer.
Parameters:
- name: Timer identifier
- interval: Callback interval in seconds
- callback: Function to call
- repeat: Number of times to repeat (None for infinite)
- pause: Start timer paused
"""
def start(self) -> None:
"""Start the timer."""
def stop(self) -> None:
"""Stop the timer."""
def pause(self) -> None:
"""Pause the timer."""
def resume(self) -> None:
"""Resume paused timer."""
def reset(self) -> None:
"""Reset timer to initial state."""
# Properties
time: float # Current time
next_fire: float # Time of next callback
is_active: bool # Whether timer is running
TimerCallback = Callable[[Timer], None]
"""Type alias for timer callback functions."""def get_app() -> App:
"""
Get the currently active Textual application.
Returns:
Active App instance
Raises:
RuntimeError: If no app is active
"""
def set_title(title: str) -> None:
"""
Set terminal window title.
Parameters:
- title: Window title text
"""
class DevConsole:
"""Development console for runtime debugging."""
def __init__(self, app: App):
"""
Initialize dev console.
Parameters:
- app: Application to debug
"""
def run_code(self, code: str) -> Any:
"""
Execute Python code in app context.
Parameters:
- code: Python code to execute
Returns:
Execution result
"""
def inspect_widget(self, selector: str) -> dict:
"""
Inspect widget properties.
Parameters:
- selector: CSS selector for widget
Returns:
Widget property dictionary
"""import pytest
from textual.app import App
from textual.widgets import Button, Input
class TestApp(App):
def compose(self):
yield Input(placeholder="Enter text", id="input")
yield Button("Submit", id="submit")
yield Button("Clear", id="clear")
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "submit":
input_widget = self.query_one("#input", Input)
self.log(f"Submitted: {input_widget.value}")
elif event.button.id == "clear":
input_widget = self.query_one("#input", Input)
input_widget.value = ""
@pytest.mark.asyncio
async def test_app_interaction():
"""Test basic app interaction with Pilot."""
app = TestApp()
async with app.run_test() as pilot:
# Type in input field
await pilot.click("#input")
await pilot.press("h", "e", "l", "l", "o")
# Verify input value
input_widget = app.query_one("#input", Input)
assert input_widget.value == "hello"
# Click submit button
await pilot.click("#submit")
# Click clear button and verify input is cleared
await pilot.click("#clear")
assert input_widget.value == ""
@pytest.mark.asyncio
async def test_keyboard_shortcuts():
"""Test keyboard shortcuts."""
app = TestApp()
async with app.run_test() as pilot:
# Enter text and use keyboard shortcut
await pilot.press("tab") # Focus input
await pilot.press("w", "o", "r", "l", "d")
await pilot.press("enter") # Should trigger submit
input_widget = app.query_one("#input", Input)
assert input_widget.value == "world"from textual.app import App
from textual.screen import Screen
from textual.widgets import Button, Static
class SecondScreen(Screen):
def compose(self):
yield Static("Second Screen")
yield Button("Go Back", id="back")
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "back":
self.dismiss("result from second screen")
class NavigationApp(App):
def compose(self):
yield Static("Main Screen")
yield Button("Go to Second", id="goto")
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "goto":
self.push_screen(SecondScreen())
@pytest.mark.asyncio
async def test_screen_navigation():
"""Test screen stack navigation."""
app = NavigationApp()
async with app.run_test() as pilot:
# Start on main screen
assert len(app.screen_stack) == 1
# Navigate to second screen
await pilot.click("#goto")
await pilot.wait_for_screen(SecondScreen)
assert len(app.screen_stack) == 2
assert isinstance(app.screen, SecondScreen)
# Go back to main screen
await pilot.click("#back")
# Should be back on main screen
assert len(app.screen_stack) == 1from textual.app import App
from textual.command import Provider, Hit
from textual.widgets import Static
class CustomCommands(Provider):
"""Custom command provider."""
async def search(self, query: str) -> list[Hit]:
"""Search for custom commands."""
commands = [
("hello", "Say hello", self.say_hello),
("goodbye", "Say goodbye", self.say_goodbye),
("time", "Show current time", self.show_time),
]
# Filter commands by query
matches = []
for name, description, callback in commands:
if query.lower() in name.lower():
score = 100 - len(name) # Shorter names score higher
hit = Hit(
score,
f"[bold]{name}[/bold] - {description}",
command=callback,
help_text=description
)
matches.append(hit)
return matches
async def say_hello(self) -> None:
"""Say hello command."""
app = self.app
status = app.query_one("#status", Static)
status.update("Hello from command palette!")
async def say_goodbye(self) -> None:
"""Say goodbye command."""
app = self.app
status = app.query_one("#status", Static)
status.update("Goodbye from command palette!")
async def show_time(self) -> None:
"""Show time command."""
import datetime
app = self.app
status = app.query_one("#status", Static)
current_time = datetime.datetime.now().strftime("%H:%M:%S")
status.update(f"Current time: {current_time}")
class CommandApp(App):
COMMANDS = {CustomCommands} # Register command provider
def compose(self):
yield Static("Press Ctrl+P to open command palette", id="status")
def on_mount(self):
# Command palette is automatically available with Ctrl+P
passfrom textual.app import App
from textual.widgets import Button, ProgressBar, Static
from textual import work
import asyncio
class WorkerApp(App):
def compose(self):
yield Static("Click button to start background task")
yield Button("Start Task", id="start")
yield Button("Cancel Task", id="cancel")
yield ProgressBar(id="progress")
@work(exclusive=True) # Cancel previous workers
async def long_running_task(self):
"""Background task that updates progress."""
progress_bar = self.query_one("#progress", ProgressBar)
for i in range(100):
# Check if cancelled
if self.workers.get("long_running_task").is_cancelled:
break
# Update progress
progress_bar.progress = i + 1
# Simulate work
await asyncio.sleep(0.1)
# Task completed
self.log("Background task completed!")
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "start":
# Start background worker
self.long_running_task()
elif event.button.id == "cancel":
# Cancel running workers
worker = self.workers.get("long_running_task")
if worker and not worker.is_finished:
worker.cancel()
self.log("Task cancelled")from textual.app import App
from textual.widgets import Button
from textual import log
class LoggingApp(App):
def compose(self):
yield Button("Info", id="info")
yield Button("Warning", id="warning")
yield Button("Error", id="error")
yield Button("Debug", id="debug")
def on_button_pressed(self, event: Button.Pressed):
button_id = event.button.id
if button_id == "info":
log.info("This is an info message")
elif button_id == "warning":
log.warning("This is a warning message")
elif button_id == "error":
log.error("This is an error message")
elif button_id == "debug":
log.debug("This is a debug message")
# Log with structured data
log("Button pressed", button=button_id, timestamp=time.time())
# Verbose logging
log.verbose("Detailed information about button press",
widget=event.button,
coordinates=(event.button.region.x, event.button.region.y))from textual.app import App
from textual.widgets import Static
from textual.timer import Timer
import datetime
class ClockApp(App):
def compose(self):
yield Static("", id="clock")
def on_mount(self):
# Update clock every second
self.set_interval(1.0, self.update_clock)
# Initial update
self.update_clock()
def update_clock(self):
"""Update the clock display."""
current_time = datetime.datetime.now().strftime("%H:%M:%S")
clock = self.query_one("#clock", Static)
clock.update(f"Current time: {current_time}")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