A declarative, extensible forms library for Textual TUI applications.
- Declarative Syntax: Define forms using class-based field declarations
- Flexible Widget Assignment: Use different widgets for the same field type
- Easy to Extend: Add custom field types and widgets with clear patterns
- Built-in Validation: Uses standard textual validators
- Simple Integration: Drop forms into existing Textual apps with minimal code
This package is in a beta stage. No details should be regarded as final, particularly naming details, but existing functionality won't be broken without a good reason. Nevertheless we believe it is sufficiently close to its final form to be rigorously exercised.
Form layout is at present embedded in the Form.render method, which returns a RenderedForm widget making it difficult to provide flexibility in representation. This mechanism has extremely low "textuality," and will be replaced in release 0.9.
LLMs have been used in the development of this package, which is subjected to serious scrutiny by a seasoned professional programmer, but this is no guarantee of quality or correctness. caveat usor.
Some of the current tests are of dubious value, and may ultimately be removed.
If you have uv installed you can run the examples from the repository using the command
uvx --from git+https://github.com/holdenweb/textual-wtf.git textual-wtf
You will see a menu of demonstration examples, each of which is also a standalone program in the src/textual_wtf/demo directory. as shown below.
| Menu item | Program name | Description |
|---|---|---|
| 1. Basic Form | basic_form.py |
A simple form with basic fields |
| 2. Advanced Form | advanced_form.py |
A more complex form with some validations |
| 3. User Registration | user_registration.py |
Rendered form integrated with other components |
| 4. Composition Example | nested_once_form.py |
Simple nested form demonstration |
| 5. Multiply Included Example | nested_twice_form.py |
Demonstrating form component re-use |
In a clone of the repository the command uv run examples/<name> should suffice.
poetry users should find that they can create a virtual environment and run
poetry run textual-wtf to start the demo. You can also build a distribution
(wheel and sdist) of the project with poetry build and install that
wherever required. No further testing in other development environments has
so far been reported.
The package will shortly be released to PyPI at version 0.8. Until then, install from this repository as follows.
python -m pip install textual_wtf@git+https://github.com/holdenweb/textual-wtf.gituv users can use
uv add textual_wtf@git+https://github.com/holdenweb/textual-wtf.gitThe most basic forms contain one or more fields. When a form is submitted a
Form.Submitted event is posted, whose form attribute allows access to
values using field names. Here's a simple example.
from textual_wtf import Form, StringField, IntegerField, BooleanField
class UserForm(Form):
"""Simple user registration form"""
name = StringField(label="Name", required=True)
age = IntegerField(label="Age", min_value=0, max_value=130)
active = BooleanField(label="Active User")With a little styling this is how the rendered form appears after it's been filled out.
Fields are named from the class variable to which they are assigned (in the
example above, "name", "age" and "active"). With the content shown the
form's get_data method will return {'name': 'Steve Holden', 'age': 178; 'active': True}. Note that the integer field has been converted to an
int, and the Boolean field to a bool.
Forms are themselves reusable components so a form can contain sub-forms, as
demonstrated in the nested_once_form.py example, to as many levels as
required.
A form can optionally be given a prefix, which is prepended to the names of
its fields with a "_" suffix. This lets you include multiple instances of the
same sub-form, as the nested_twice_form.py example demonstrates.
In the event of two fields receiving the same (fully-qualified) name an exception is raised when the form is created.
When a form is rendered "Cancel" and "Submit" buttons are added at the
bottom of the form. When clicked these buttons raise Form.Cancelled
and Form.Submitted events respectively; each has a form attribute
which can be used to access the form.
This behaviour will be more configurable in later releases.
Once rendered, the simplest way to access the form's data is
by calling its get_data method.
This returns a dictionary where fields' values are keyed by their
fully-qualified names (i.e. including any sub-form prefixes,
with underscores separating the named components).
Users may find that in dealing with complex nested forms it
becomes tedious to use fully-qualified names.
An experimental get_field method takes a string argument. If that string is
the fully-qualified name of a field, or if there is only one field whose
fully-qualified name ends an underscore followed by the string, then it
returns a field object whose attributes include name and value.
This access mechanism is perhaps the most fluid part of the current design, and discussions (feel free to raise a Github issue to start a discussion - we'll move it into Discussions if it looks like developing) are encouraged. For example, would it more usable to implement a read/write property with dotted access? Should alternatives be offered?
StringField- Text input (single line)TextField- Text input (multi-line)IntegerField- Integer input with validationBooleanField- CheckboxChoiceField- Select dropdown
Documentation of the foundational classes (Forms, Fields and Widgets) is the
biggest current technical debt. The examples directory
contains some code that we hope will help you to evalute textual-wtf
and shape its direction with your feedback.
Additional example programs, particularly those demonstrating the styling possibilities, would be especially valuable.
# Create a venv with dev dependencies
uv venv
# Run tests
uv run pytest
Run specific test
uv run pytest tests/test_fields.pyThe library uses a three-layer architecture:
- Fields - Handle data conversion and validation logic
- Widgets - Handle UI rendering and user interaction
- Forms - Coordinate fields and widgets into complete forms
No detailed architecture documentation is presently available.
MIT License - see LICENSE file for details.
When using ChoiceField, provide choices as a list of (value, label) tuples:
country = ChoiceField(
label="Country",
choices=[
("us", "United States"), # value, label
("uk", "United Kingdom"),
("ca", "Canada"),
]
)- The value (first element) is what gets stored in the form data
- The label (second element) is what the user sees in the dropdown
When the form is submitted, form.get_data()['country'] will contain the
value (e.g., "us"), not the label.
The prehistory of the project is preserved in the prototype branch,
a somewhat chaotic mishmash of code that nevertheless proved the basic ideas
embodied in the current design to be usable in practice.
