Textual UI framework for Genro Bag-driven applications
Project description
genro-textual
Declarative Terminal UI framework built on Textual and powered by genro-builders.
Define your UI as a "recipe" — widgets, CSS, key bindings, data binding — all as Bag nodes. The compiler transforms the recipe into a live Textual app with reactive data binding.
Status: Pre-Alpha — Active development.
Installation
pip install genro-textual
Quick Start
from genro_textual import TextualApp
class Application(TextualApp):
def recipe(self, page):
page.binding(key="q", action="quit", description="Quit")
page.static("^greeting")
page.input(value="^form.name", placeholder="Your name")
page.button("OK")
def setup(self):
self.data["greeting"] = "Hello, World!"
self.data["form.name"] = ""
super().setup()
if __name__ == "__main__":
Application().run()
Type a name in the input, press Tab — the greeting doesn't change (it's on a different path), but the data Bag updates. Bind the Static and Input to the same path to see reactive updates.
Architecture
genro-textual follows the puppeteer/puppet pattern:
- TextualApp (the puppeteer) — configures recipe, data, compiler
- LiveApp (the puppet) — a bare
textual.app.Appdriven by the puppeteer - CompiledBag — the script, kept in sync by the BindingManager
┌──────────────┐ compile ┌──────────────┐ render ┌──────────────┐
│ Source Bag │ ──────────────► │ Compiled Bag │ ─────────────► │ LiveApp │
│ (recipe) │ │ (expanded, │ │ (Textual) │
│ │ │ resolved) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
▲ │ │
│ BindingManager │
│ (data → widget) │
│ │
│ blur / change │
└─────────────────────── (widget → data) ───────────────────────┘
Data Binding
Read: Data to Widget
Bind widget values to data using ^path syntax:
page.static("^user.name") # value bound to data path
page.input(value="^form.email") # attribute bound to data path
When data["user.name"] changes, the Static updates automatically.
Write: Widget to Data
- Input — writes to data on blur (Tab, click away), not on every keystroke
- Checkbox / Switch — writes to data on change (immediate)
The _reason mechanism prevents infinite loops: when a widget writes to data, the BindingManager skips updating that same widget.
Bidirectional Example
class Application(TextualApp):
def recipe(self, page):
page.binding(key="q", action="quit", description="Quit")
page.input(value="^form.name", placeholder="Name")
page.input(value="^form.surname", placeholder="Surname")
page.static("^form.name") # updates when Input blurs
page.static("^form.surname")
page.button("OK")
def setup(self):
self.data["form.name"] = "John"
self.data["form.surname"] = "Doe"
super().setup()
CSS
Inline Stylesheets
CSS in the recipe, with Textual theme variables:
page.css("""
.title { color: green; text-style: bold; }
#sidebar { width: 30; background: $surface; border-left: solid $primary; }
""")
Direct Style Attributes
CSS properties can be set directly on widgets and bound to data:
page.vertical(id="panel", width="^_system.panel.width", display="^_system.panel.display")
When data["_system.panel.width"] changes, the widget resizes. Style attributes are classified automatically at mount time:
- Constructor parameters →
widget.__init__ - CSS properties →
widget.styles - Reactive attributes →
widget.set_reactive
Note: CSS variables ($surface, $primary) work only in page.css(), not in direct attributes.
Inspector Drawer
Built-in inspector for debugging Bag structures at runtime:
class Application(TextualApp):
def recipe(self, page):
page.header()
main = page.horizontal(id="main-area")
content = main.verticalscroll(id="main-content")
content.static("My app content")
# Drawer with inspector
drawer = main.vertical(
id="drawer",
width="^_system.drawer.width",
display="^_system.drawer.display",
)
tabs = drawer.tabbedcontent()
tabs.tabpane(title="Data").tree(label="data", store=self.data)
tabs.tabpane(title="Source").tree(label="source", store=self.source)
tabs.tabpane(title="Compiled").tree(label="compiled", store=self.compiled)
page.footer()
The Tree widget with store attribute populates recursively from a Bag and updates reactively when the Bag changes, preserving expanded state.
Use _system paths for infrastructure data (drawer width, display) to separate them from application data.
Key Bindings
page.binding(key="q", action="quit", description="Quit")
page.binding(key="f12", action="toggle_drawer", description="Inspector")
Bindings appear in the Footer and are clickable.
Components
Reusable UI blocks defined with @component:
from genro_builders.builder import component
from genro_textual import TextualApp, TextualBuilder
class MyMixin:
@component(sub_tags="")
def login_form(self, comp, title="Login", **kwargs):
comp.static(title)
comp.input(placeholder="Username", value="^.username")
comp.input(placeholder="Password", value="^.password")
comp.button("Submit", variant="primary")
class MyBuilder(MyMixin, TextualBuilder):
pass
class Application(TextualApp):
builder_class = MyBuilder
def recipe(self, page):
page.login_form(title="Sign In")
Live REPL
Connect to a running app and modify it in real-time:
# Terminal 1: Start the app
pygui run examples/complex_app.py
# Terminal 2: Connect to it
pygui connect complex_app
>>> app.data["form.name"] = "New value"
>>> app.page.static("Added from REPL!")
CLI Reference
| Command | Description |
|---|---|
pygui run FILE.py |
Run a TextualApp |
pygui run -r FILE.py |
Run with auto-reload (watches for file changes) |
pygui run -c FILE.py |
Run and connect REPL in tmux split |
pygui list |
List registered running apps |
pygui connect NAME |
Connect REPL to a running app |
pygui stop NAME |
Stop a running app |
pygui completions zsh |
Generate shell completions |
Supported Widgets
genro-textual supports 60+ Textual elements:
Containers
container, vertical, horizontal, center, middle, grid, verticalscroll, horizontalscroll, scrollablecontainer, verticalgroup, horizontalgroup, itemgrid
Input Widgets
button, checkbox, input, maskedinput, switch, select, selectionlist, optionlist, radiobutton, radioset, textarea
Display Widgets
static, label, link, header, footer, rule, markdown, markdownviewer, richlog, log, pretty, digits, sparkline, progressbar, placeholder, loadingindicator, welcome
Complex Widgets
tabbedcontent, tabpane, tabs, tab, datatable (with column, row), tree (with store), directorytree, listview, listitem, collapsible, contentswitcher, helppanel, keypanel
App Configuration
css, binding
Components
fieldset, form
License
Apache License 2.0 — See LICENSE for details.
Copyright 2025 Softwell S.r.l.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file genro_textual-0.3.0.tar.gz.
File metadata
- Download URL: genro_textual-0.3.0.tar.gz
- Upload date:
- Size: 61.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5710625649ca86d3b32b4f1df595336980f8fd96dfc7e4b1c2f8fc17d65ad77a
|
|
| MD5 |
4c70ee1eea35613eba88502d73231dae
|
|
| BLAKE2b-256 |
3daf8c436e099db9107703cd96bdc8f3f0606465147e8c8206b9f639184a56c2
|
Provenance
The following attestation bundles were made for genro_textual-0.3.0.tar.gz:
Publisher:
publish.yml on genropy/genro-textual
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
genro_textual-0.3.0.tar.gz -
Subject digest:
5710625649ca86d3b32b4f1df595336980f8fd96dfc7e4b1c2f8fc17d65ad77a - Sigstore transparency entry: 1180617447
- Sigstore integration time:
-
Permalink:
genropy/genro-textual@b5d91ccc1ff6ac28d8c5417ca6147e949c29a7db -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b5d91ccc1ff6ac28d8c5417ca6147e949c29a7db -
Trigger Event:
push
-
Statement type:
File details
Details for the file genro_textual-0.3.0-py3-none-any.whl.
File metadata
- Download URL: genro_textual-0.3.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93b6e82fa9e75ad780836720dff0ec7f73962f579f6aa2d13715bda31b5815bf
|
|
| MD5 |
e0c1f70dffdf380b4a579846e6b90c34
|
|
| BLAKE2b-256 |
4f2ba3d68362927569b8056c6f79fbfe486fb32f485b4e75aabf101f3fd96501
|
Provenance
The following attestation bundles were made for genro_textual-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on genropy/genro-textual
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
genro_textual-0.3.0-py3-none-any.whl -
Subject digest:
93b6e82fa9e75ad780836720dff0ec7f73962f579f6aa2d13715bda31b5815bf - Sigstore transparency entry: 1180617566
- Sigstore integration time:
-
Permalink:
genropy/genro-textual@b5d91ccc1ff6ac28d8c5417ca6147e949c29a7db -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b5d91ccc1ff6ac28d8c5417ca6147e949c29a7db -
Trigger Event:
push
-
Statement type: