Skip to main content

Declarative forms library for Textual TUI applications

Project description

textual-wtf

Declarative forms library for Textual TUI applications.

This is a complete revision of the library to improve maintainability, consistency and usability for developers. While still technically pre-release you can now write code with reasonable confidence that it's not going to need too many changes in the future.

Major improvements in this release

Decoupled Form class and instances from the widget hierarchy

Form instances are now properly constructed, creating a BoundField for each Field in the class definition to hold the instance-specific data and remove the confusing sharing of class attributes. BoundField became a plain Python object rather than a Textual widget. A separate FieldController now owns all mutable state (value, errors, dirty flag, listeners), and a new FieldWidget (Container) handles the Textual-side composition. This separation makes BoundField usable outside a mounted app — for testing and programmatic validation — and eliminates the fragility of mixing reactive state with widget lifetime.

simple_layout() / __call__() rendering split

BoundField now offers two rendering modes: simple_layout() returns a fully composed FieldWidget (label + input + help + error chrome), while __call__() returns just the raw inner widget for full layout freedom. Both accept per-render overrides for label_style, help_style, disabled, and required.

label_style and help_style

Three label modes ("above", "beside", "placeholder") and two help-text modes ("below", "tooltip") are configurable at form class, form instance, field, and render-call levels.

Unified validation on the Textual Validator pattern

Validators became proper Validator subclasses with validate() methods. Convenience kwargs (required=, min_length=, max_length=, min_value=, max_value=) were added to field constructors. Event-scoped validation (validate_on) lets validators fire only on blur, change, or submit.

Form instance embedding

ComposedForm markers replaced with direct assignment of Form instances as class attributes, with a required= cascade: field-level explicit pin → form class attribute → form instance kwarg → render kwarg.

TabbedForm widget

A Widget taking sub-forms and rendering each in a TabPane via TabbedContent. Tab labels turn $error colour when any field in that tab has a validation error.

title= kwarg on BaseForm

Instance-level title override, used as the TabbedForm tab label and as a heading in DefaultFormLayout.

Example code and MkDocs documentation

A small multi-screen demo app and a complete MkDocs + Material docs site: two guide sections (7 pages), API reference with mkdocstrings (7 pages), and how-to recipes (4 pages).

Installation

uv sync

Running tests

uv run pytest -v

Quick start

from textual.app import App, ComposeResult
from textual_wtf import Form, StringField, IntegerField, BooleanField
from textual_wtf.forms import BaseForm


class ContactForm(Form):
    title = "Contact"
    name = StringField("Name", required=True)
    age = IntegerField("Age", min_value=0, max_value=150)
    active = BooleanField("Active")


class MyApp(App):
    def compose(self) -> ComposeResult:
        self.form = ContactForm()
        yield self.form.build_layout()

    def on_base_form_submitted(self, event: BaseForm.Submitted) -> None:
        data = event.form.get_data()
        self.notify(f"Submitted: {data}")

    def on_base_form_cancelled(self, event: BaseForm.Cancelled) -> None:
        self.exit()


if __name__ == "__main__":
    MyApp().run()

Embedded forms

class AddressForm(Form):
    street = StringField("Street")
    city = StringField("City")

class OrderForm(Form):
    billing = AddressForm()
    shipping = AddressForm()
    notes = TextField("Notes")

Cross-field validation

Override clean_form() for validation that spans multiple fields:

class PasswordForm(Form):
    password = StringField("Password", required=True)
    confirm = StringField("Confirm", required=True)

    def clean_form(self):
        if self.password.value != self.confirm.value:
            self.add_error("confirm", "Passwords do not match")
            return False
        return True

Custom layouts

Subclass FormLayout and override compose():

from textual.containers import Horizontal
from textual.widgets import Button
from textual_wtf import FormLayout

class TwoColumnLayout(FormLayout):
    def compose(self):
        with Horizontal():
            yield self.form.first_name.simple_layout(label_style="above")
            yield self.form.last_name.simple_layout(label_style="above")
        yield self.form.email.simple_layout()
        with Horizontal(id="buttons"):
            yield Button("Submit", id="submit", variant="primary")
            yield Button("Cancel", id="cancel")

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

textual_wtf-0.10.0a5.tar.gz (197.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

textual_wtf-0.10.0a5-py3-none-any.whl (45.2 kB view details)

Uploaded Python 3

File details

Details for the file textual_wtf-0.10.0a5.tar.gz.

File metadata

  • Download URL: textual_wtf-0.10.0a5.tar.gz
  • Upload date:
  • Size: 197.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for textual_wtf-0.10.0a5.tar.gz
Algorithm Hash digest
SHA256 d39572d86ca853971bd8793faa66b585c73e284617dc158722ec8f296ac82257
MD5 3af0704028f2010b18c6611af8efd79f
BLAKE2b-256 3a8e31ee33a03b4daad5327ff4488de0bfbce17fa9be3fc8058aab6468de1b23

See more details on using hashes here.

File details

Details for the file textual_wtf-0.10.0a5-py3-none-any.whl.

File metadata

  • Download URL: textual_wtf-0.10.0a5-py3-none-any.whl
  • Upload date:
  • Size: 45.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for textual_wtf-0.10.0a5-py3-none-any.whl
Algorithm Hash digest
SHA256 12d1801af1b227ae8c30e159a7a383d2bd8f2e869321d0c6254da01a58f89049
MD5 3cd51872554ea6909dc2af77c63d8991
BLAKE2b-256 52fb8967c48b727abe6ffc18bac0ea088cccf0bdec8e2a868f9bf330f62ffb2e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page