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.
App Shell
The app_shell component provides a complete application layout with header, scrollable content area, resizable inspector drawer (Data/Source/Compiled tree tabs), and footer:
class Application(TextualApp):
def recipe(self, page):
shell = page.app_shell(
title="My App",
data_store=self.data,
source_store=self.source,
compiled_store=self.compiled,
)
shell.content.static("Hello!")
shell.content.input(value="^form.name", placeholder="Name")
def setup(self):
self._init_shell_data()
self.data["form.name"] = "John"
super().setup()
The content slot is a named insertion point — widgets added to shell.content are mounted inside the scrollable content area. Press F12 to toggle the inspector drawer.
app_shell is defined in FoundationMixin, included in TextualBuilder by default. To exclude it, compose your own builder without the mixin.
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 in mixin classes. Component mixins live in genro_textual.components.
Simple component (no slots)
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")
Component with named slots
Components can declare named slots — insertion points where the caller adds content:
class DashboardMixin:
@component(sub_tags="*", slots=["left", "right"])
def dashboard(self, comp, title="", **kwargs):
comp.static(title)
main = comp.horizontal()
left_node = main.vertical(id="left-panel")
right_node = main.vertical(id="right-panel")
return {"left": left_node, "right": right_node}
class MyBuilder(DashboardMixin, TextualBuilder):
pass
Usage in recipe:
def recipe(self, page):
dash = page.dashboard(title="Overview")
dash.left.tree(label="nav", store=self.data)
dash.right.static("Main content")
The handler body returns a dict mapping slot names to destination nodes. Content added to slots at recipe time is mounted into those nodes at compile time.
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, app_shell (with content slot)
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.4.0.tar.gz.
File metadata
- Download URL: genro_textual-0.4.0.tar.gz
- Upload date:
- Size: 62.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8e099250de6ff1824829dd1957d79479305147f7a806ffa18331826bbf6a4a9
|
|
| MD5 |
139bb36ca2f1fe8572b06bb9b347f832
|
|
| BLAKE2b-256 |
b21e63f5bd9bd63bb1cb47a239be568dc0863d8b01d9094966c6cc8c18dd3533
|
Provenance
The following attestation bundles were made for genro_textual-0.4.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.4.0.tar.gz -
Subject digest:
e8e099250de6ff1824829dd1957d79479305147f7a806ffa18331826bbf6a4a9 - Sigstore transparency entry: 1181711082
- Sigstore integration time:
-
Permalink:
genropy/genro-textual@2cfd800d9b728cbd3a3e05f76430c9652b53d5fc -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2cfd800d9b728cbd3a3e05f76430c9652b53d5fc -
Trigger Event:
push
-
Statement type:
File details
Details for the file genro_textual-0.4.0-py3-none-any.whl.
File metadata
- Download URL: genro_textual-0.4.0-py3-none-any.whl
- Upload date:
- Size: 32.4 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 |
c391ae23cc5b5ca3fdf707cf1b885e7de34896c74912d5f6a55f8f8d8d0fb202
|
|
| MD5 |
0a3a21053e03a7b1bae186e322269662
|
|
| BLAKE2b-256 |
772f3747233cfcfc0060026702b27a3ad177c98938083a686beac7db4b07f63d
|
Provenance
The following attestation bundles were made for genro_textual-0.4.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.4.0-py3-none-any.whl -
Subject digest:
c391ae23cc5b5ca3fdf707cf1b885e7de34896c74912d5f6a55f8f8d8d0fb202 - Sigstore transparency entry: 1181711086
- Sigstore integration time:
-
Permalink:
genropy/genro-textual@2cfd800d9b728cbd3a3e05f76430c9652b53d5fc -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2cfd800d9b728cbd3a3e05f76430c9652b53d5fc -
Trigger Event:
push
-
Statement type: