or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-app.mddata-structures.mderrors-edge-cases.mdindex.mdpatterns-recipes.mdprompts-api.mdquick-reference.mdwidgets-api.md
tile.json

patterns-recipes.mddocs/

Patterns & Recipes

Common task patterns and recipes for inquirer-textual.

Basic Patterns

Always Check Command Before Using Value

result = prompts.text("Input:")
# ALWAYS check command first
if result.command == 'select':
    # Safe to use result.value
    process(result.value)
elif result.command == 'quit':
    print("User quit")
    exit(0)

Standard Command Handling

def handle_result(result):
    """Standard result handler."""
    if result.command == 'quit':
        # Critical exit
        exit(0)
    elif result.command == 'cancel':
        return None
    elif result.command == 'select':
        return result.value
    else:
        # Unknown command
        return None

Reusable Shortcuts

# Define once, use everywhere
COMMON_SHORTCUTS = [
    Shortcut('escape', 'cancel', 'Cancel'),
    Shortcut('ctrl+h', 'help', 'Help')
]

result1 = prompts.text("Field 1:", shortcuts=COMMON_SHORTCUTS)
result2 = prompts.text("Field 2:", shortcuts=COMMON_SHORTCUTS)

Validation Patterns

Custom Validator Template

from textual.validation import Validator, ValidationResult

class CustomValidator(Validator):
    def __init__(self, param):
        super().__init__()
        self.param = param

    def validate(self, value: str) -> ValidationResult:
        if condition_met(value, self.param):
            return self.success()
        return self.failure("Error message")

result = prompts.text("Input:", validators=CustomValidator(param))

Common Validators

from textual.validation import Function

# Email validator
email_validator = Function(
    lambda s: '@' in s and '.' in s,
    "Invalid email format"
)

# Min length validator
min_length = lambda n: Function(
    lambda s: len(s) >= n,
    f"Minimum {n} characters"
)

# No spaces validator
no_spaces = Function(
    lambda s: ' ' not in s,
    "No spaces allowed"
)

# Alphanumeric validator
alphanumeric = Function(
    lambda s: s.isalnum(),
    "Only letters and numbers allowed"
)

result = prompts.text("Username:", validators=[min_length(3), no_spaces, alphanumeric])

Validator Factory

from textual.validation import Validator, ValidationResult

def create_range_validator(min_val: int, max_val: int) -> Validator:
    """Create validator for numeric range."""
    class RangeValidator(Validator):
        def validate(self, value: str) -> ValidationResult:
            try:
                num = int(value)
                if min_val <= num <= max_val:
                    return self.success()
                return self.failure(f"Must be between {min_val} and {max_val}")
            except ValueError:
                return self.failure("Must be a number")
    return RangeValidator()

result = prompts.text("Age:", validators=create_range_validator(0, 120))

Post-Prompt Validation

def get_validated_input():
    """Get input with post-prompt validation and retry."""
    while True:
        result = prompts.text("Enter positive number:")

        if result.command != 'select':
            return None

        try:
            num = int(result.value)
            if num > 0:
                return num
            else:
                print("Must be positive")
        except ValueError:
            print("Must be a number")

value = get_validated_input()

Choice Patterns

Choice Factory

from inquirer_textual.common.Choice import Choice

def create_choices_from_dict(items):
    """Create Choice objects from dictionary list."""
    return [
        Choice(item['display_name'], data=item)
        for item in items
    ]

items = [
    {'display_name': 'Option A', 'id': 1, 'config': {...}},
    {'display_name': 'Option B', 'id': 2, 'config': {...}}
]

choices = create_choices_from_dict(items)
result = prompts.select("Choose:", choices)
if result.command == 'select':
    item_id = result.value.data['id']

Choice with Actions

from inquirer_textual.common.Choice import Choice

actions = [
    Choice("View", command="view"),
    Choice("Edit", command="edit"),
    Choice("Delete", command="delete"),
    Choice("Cancel", command="cancel")
]

result = prompts.select("Action:", actions)

action_handlers = {
    'view': lambda: view_item(),
    'edit': lambda: edit_item(),
    'delete': lambda: delete_item(),
    'cancel': lambda: None
}

if result.command in action_handlers:
    action_handlers[result.command]()

Type-Safe Choice Access

from inquirer_textual.common.Choice import Choice

result = prompts.select("Pick:", [Choice("A", data={"id": 1}), Choice("B", data={"id": 2})])

if result.command == 'select':
    if isinstance(result.value, Choice):
        # Type-safe access to data
        item_id: int = result.value.data["id"]
    else:
        # Handle string case
        item_name: str = result.value

Multi-Prompt Patterns

Fixed Form

def collect_user_data():
    """Collect user data with fixed form."""
    widgets = [
        InquirerText("First name:"),
        InquirerText("Last name:"),
        InquirerText("Email:"),
        InquirerNumber("Age:")
    ]

    result = prompts.multi(widgets)

    if result.command == 'select':
        first, last, email, age_str = result.value
        return {
            'first_name': first,
            'last_name': last,
            'email': email,
            'age': int(age_str)
        }
    return None

data = collect_user_data()

Dynamic Form

def build_dynamic_form(field_specs):
    """Build form from field specifications."""
    widgets = []

    for spec in field_specs:
        if spec['type'] == 'text':
            widgets.append(InquirerText(spec['label']))
        elif spec['type'] == 'number':
            widgets.append(InquirerNumber(spec['label']))
        elif spec['type'] == 'select':
            widgets.append(InquirerSelect(spec['label'], spec['choices']))
        elif spec['type'] == 'confirm':
            widgets.append(InquirerConfirm(spec['label'], default=spec.get('default', False)))

    result = prompts.multi(widgets)
    return result.value if result.command == 'select' else None

fields = [
    {'type': 'text', 'label': 'Name:'},
    {'type': 'select', 'label': 'Country:', 'choices': ['US', 'UK', 'CA']},
    {'type': 'confirm', 'label': 'Subscribe:', 'default': True}
]

data = build_dynamic_form(fields)

inquiry_func Patterns

Helper Functions

def ask_text(app, message, default='', validators=None, shortcuts=None):
    """Reusable text prompt helper."""
    widget = InquirerText(message, default=default, validators=validators)
    result = app.prompt(widget, shortcuts=shortcuts)
    return result.value if result.command == 'select' else None

def ask_select(app, message, choices, default=None, shortcuts=None):
    """Reusable select prompt helper."""
    widget = InquirerSelect(message, choices, default=default)
    result = app.prompt(widget, shortcuts=shortcuts)
    return result.value if result.command == 'select' else None

def ask_confirm(app, message, default=False):
    """Reusable confirm prompt helper."""
    widget = InquirerConfirm(message, default=default)
    result = app.prompt(widget)
    return result.value if result.command == 'select' else False

def registration_flow(app):
    name = ask_text(app, "Name:")
    if not name:
        return

    email = ask_text(app, "Email:")
    if not email:
        return

    subscribe = ask_confirm(app, "Subscribe?", default=True)

    app.stop({'name': name, 'email': email, 'subscribe': subscribe})

State Management

class FlowState:
    def __init__(self):
        self.data = {}
        self.current_step = 0

    def add(self, key, value):
        self.data[key] = value
        self.current_step += 1

    def get_data(self):
        return self.data

def stateful_flow(app):
    state = FlowState()

    name_result = app.prompt(InquirerText("Name:"))
    if name_result.command != 'select':
        return
    state.add('name', name_result.value)

    email_result = app.prompt(InquirerText("Email:"))
    if email_result.command != 'select':
        return
    state.add('email', email_result.value)

    app.stop(state.get_data())

Wizard with Navigation

def wizard_flow(app):
    steps = []

    # Step 1
    name_result = app.prompt(InquirerText("Name:"))
    if name_result.command != 'select':
        return
    steps.append(('name', name_result.value))

    # Step 2
    role_result = app.prompt(InquirerSelect("Role:", ["Dev", "Designer", "Manager"]))
    if role_result.command != 'select':
        return
    steps.append(('role', role_result.value))

    # Confirm
    confirm_result = app.prompt(InquirerConfirm("Submit?"))
    if confirm_result.command == 'select' and confirm_result.value:
        app.stop(dict(steps))

Task Recipes

Login Form

def login():
    """Simple login form."""
    username_result = prompts.text("Username:")
    if username_result.command != 'select':
        return None

    password_result = prompts.secret("Password:")
    if password_result.command != 'select':
        return None

    return {
        'username': username_result.value,
        'password': password_result.value
    }

credentials = login()
if credentials:
    authenticate(credentials['username'], credentials['password'])

Configuration Wizard

def configure_app():
    """Application configuration wizard."""
    widgets = [
        InquirerText("App name:"),
        InquirerSelect("Environment:", ["Development", "Staging", "Production"]),
        InquirerNumber("Port:"),
        InquirerCheckbox("Features:", ["Auth", "Database", "Caching", "Logging"]),
        InquirerConfirm("Enable debug mode?", default=False)
    ]

    result = prompts.multi(widgets)

    if result.command == 'select':
        name, env, port_str, features, debug = result.value
        return {
            'name': name,
            'environment': env,
            'port': int(port_str),
            'features': features,
            'debug': debug
        }
    return None

config = configure_app()
if config:
    save_config(config)

Survey/Feedback Form

def collect_feedback():
    """User feedback survey."""
    def flow(app):
        # Satisfaction
        satisfied = app.prompt(InquirerConfirm("Satisfied with service?", default=True))

        feedback = {'satisfied': satisfied.value}

        if not satisfied.value:
            # Dissatisfied - ask for details
            issue = app.prompt(InquirerSelect(
                "Main issue:",
                ["Too slow", "Too expensive", "Missing features", "Poor support"]
            ))
            feedback['issue'] = issue.value

            details = app.prompt(InquirerText("Please elaborate:"))
            feedback['details'] = details.value
        else:
            # Satisfied - ask for rating
            rating = app.prompt(InquirerSelect(
                "Rating:",
                ["5 - Excellent", "4 - Good", "3 - Okay"]
            ))
            feedback['rating'] = rating.value

        app.stop(feedback)

    app = InquirerApp()
    result = app.run(inline=True, inquiry_func=flow)
    return result.value

feedback = collect_feedback()

Bulk Item Entry

def collect_items():
    """Collect multiple items until done."""
    def flow(app):
        items = []

        while True:
            item = app.prompt(InquirerText("Enter item (empty to finish):"))

            if item.command != 'select':
                break

            if not item.value.strip():
                break

            items.append(item.value)

            more = app.prompt(InquirerConfirm("Add another?", default=True))
            if not more.value:
                break

        app.stop(items)

    app = InquirerApp()
    result = app.run(inline=True, inquiry_func=flow)
    return result.value

items = collect_items()
print(f"Collected {len(items)} items")

Menu System

def show_menu():
    """Interactive menu system."""
    menu_items = [
        Choice("Create New", command="create"),
        Choice("View Existing", command="view"),
        Choice("Edit", command="edit"),
        Choice("Delete", command="delete"),
        Choice("Settings", command="settings"),
        Choice("Exit", command="exit")
    ]

    while True:
        result = prompts.select("Main Menu:", menu_items)

        if result.command == 'create':
            create_item()
        elif result.command == 'view':
            view_items()
        elif result.command == 'edit':
            edit_item()
        elif result.command == 'delete':
            delete_item()
        elif result.command == 'settings':
            show_settings()
        elif result.command == 'exit' or result.command == 'quit':
            break

show_menu()

Conditional Flow Based on Previous Answers

def conditional_survey():
    """Survey with conditional questions."""
    def flow(app):
        data = {}

        # Q1: User type
        user_type = app.prompt(InquirerSelect(
            "Are you a:",
            ["Student", "Professional", "Hobbyist"]
        ))
        if user_type.command != 'select':
            return
        data['user_type'] = user_type.value

        # Q2: Conditional on user type
        if user_type.value == "Student":
            school = app.prompt(InquirerText("School name:"))
            data['school'] = school.value

            year = app.prompt(InquirerSelect("Year:", ["1st", "2nd", "3rd", "4th"]))
            data['year'] = year.value

        elif user_type.value == "Professional":
            company = app.prompt(InquirerText("Company:"))
            data['company'] = company.value

            experience = app.prompt(InquirerNumber("Years of experience:"))
            data['experience'] = int(experience.value)

        # Common question for all
        interests = app.prompt(InquirerCheckbox(
            "Interests:",
            ["Python", "JavaScript", "Machine Learning", "Web Dev"]
        ))
        data['interests'] = interests.value

        app.stop(data)

    app = InquirerApp()
    result = app.run(inline=True, inquiry_func=flow)
    return result.value

data = conditional_survey()

Error Recovery Patterns

Retry on Invalid Input

def get_valid_email():
    """Get valid email with retry."""
    def flow(app):
        while True:
            email = app.prompt(InquirerText("Email:"))

            if email.command != 'select':
                return

            if '@' in email.value and '.' in email.value:
                app.stop(email.value)
                return
            else:
                # Show error and retry
                app.prompt(InquirerText("Invalid email! Press Enter to retry"))

    app = InquirerApp()
    result = app.run(inline=True, inquiry_func=flow)
    return result.value

email = get_valid_email()

Graceful Degradation

def get_config_with_fallback():
    """Get config with fallback defaults."""
    result = prompts.text("Server URL (leave empty for localhost):")

    if result.command != 'select':
        # User quit - use default
        return {'url': 'localhost'}

    url = result.value.strip() or 'localhost'
    return {'url': url}

config = get_config_with_fallback()