A simple python **tui** framework built on top of the ``[ratatui](https://ratatui.rs)`` and ``[tachyonfx](https://github.com/ratatui/tachyonfx)`` rust crates.
Project description
xnano
A simple python tui framework built on top of the ratatui and tachyonfx rust libraries.
[!IMPORTANT] The
xnanolibrary is currently going through a complete rebuild of it's primary purpose and API. As the package is still in beta, expect frequent changes from all package versions0.99.xxand above until the stable1.0.0release.Docs will soon be available at: xnano.hammad.app.
xnano is a modern, lighweight and incredibly declarative TUI framework for Python. It is built on top of the xnano-core rust library, which provides the core rendering and event handling capabilities through:
- ratatui A Rust library for building terminal user interfaces.
- tachyonfx Rust library for adding effects and animations to ratatui applications.
Furthermore, xnano itself uses the pydantic-core library for type validation and similar operations.
Installation
[!WARNING] Ensure to install the
0.99.7+ version ofxnanoto ensure correct dependency and API resolution.
pip install "xnano>=0.99.7"
Or use uv:
uv add "xnano>=0.99.7"
Examples
Hello World
The minimal xnano app. Define a Grid subclass with annotated Field slots, then pass an instance to Terminal().run(). The terminal takes over the screen, renders each frame, and cleans up on exit.
# xnano may remind you of `pydantic` in many places, this is
# one of them.
from xnano.beta import Grid, Field, Terminal
from xnano.beta.color import tailwind_color
from xnano.beta.hooks import on_tick
class App(Grid):
message: str = Field(default="Hello, world!", color=tailwind_color("sky", 500))
current_color: str = Field(default="sky", state=True)
@on_tick(1000)
def update_color(self) -> None:
if self.current_color == "sky":
self.current_color = "white"
self.grid_set_field("message", color="white")
else:
self.current_color = "sky"
self.grid_set_field("message", color=tailwind_color("sky", 500))
Terminal().run(App())
Strict Type Safety
Any field with a type annotation is set with Field(strict=True) and is validated through the pydantic-core library by default.
Layout & Nesting
Grids compose naturally — nest one Grid inside another as a Field value. Direction ("horizontal" / "vertical") and gap control how fields are laid out. Use size (absolute columns/rows or a 0.0–1.0 fraction) and flex (fill weight) to proportion each slot.
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.hooks import on_keyboard
class SidebarTitle(Grid, align="center"):
title: str = Field("This is a title.", align="center")
class Sidebar(Grid, direction="vertical"):
title: SidebarTitle = Field(default_factory=SidebarTitle, size=0.1)
nav: str = Field(default="- Home", size=0.9, flex="flex-auto")
class App(Grid, direction="horizontal", gap=1):
sidebar: Sidebar = Field(default_factory=Sidebar, size=0.25)
content: str = Field(default="Main area", flex=1, border="rounded")
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
Terminal().run(App())
Keyboard Events
Use @on_keyboard to bind methods to key names or sequences. The decorated method receives an optional Context argument that exposes the live terminal. State fields (state=True) hold app data without rendering — update them and reference them from layout fields.
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.hooks import on_keyboard
class Counter(Grid, direction="vertical", gap=1):
label: str = Field(default="Count: 0", size=1)
hint: str = Field(default="Press up/down to change, q to quit", size=1)
count: int = Field(default=0, state=True)
@on_keyboard("up")
def increment(self) -> None:
self.count += 1
self.label = f"Count: {self.count}"
@on_keyboard("down")
def decrement(self) -> None:
self.count -= 1
self.label = f"Count: {self.count}"
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
Terminal().run(Counter())
Click Handlers
Pass mouse_events=True to Terminal to enable mouse input. Use @on_click("field_name") to scope a handler to the rendered area of a specific field — the handler fires only when that region is clicked.
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.hooks import on_click, on_keyboard
class App(Grid, direction="vertical", gap=1):
button: str = Field(default="[ Click me ]", size=3, border="rounded")
status: str = Field(default="Waiting...", flex=1)
@on_click("button")
def on_button(self, ctx: Context) -> None:
self.status = "Clicked!"
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
Terminal(mouse_events=True).run(App())
Timed Updates
@on_tick(interval_ms) fires a method on a recurring timer. Use it for clocks, progress indicators, polling, or any periodic UI refresh without blocking the event loop.
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.hooks import on_tick, on_keyboard
import time
class Clock(Grid, direction="vertical"):
time_display: str = Field(default="", size=3, border="rounded")
def __post_init__(self) -> None:
self.time_display = time.strftime("%H:%M:%S")
@on_tick(1000)
def update_time(self) -> None:
self.time_display = time.strftime("%H:%M:%S")
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
Terminal().run(Clock())
State & Context Manager
Pass any object as state to Terminal to thread shared data through the session. Every Grid instance can read it via self.state. Override grid_render() to recompute field values once per frame — useful when display depends on state that changes externally.
from dataclasses import dataclass
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.hooks import on_keyboard
@dataclass
class AppState:
username: str = "guest"
class App(Grid, direction="vertical", gap=1):
header: str = Field(default="", size=1)
body: str = Field(default="Press q to quit", flex=1)
def grid_render(self) -> None:
self.header = f"Hello, {self.state.username}!"
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
with Terminal(state=AppState(username="hammad")) as t:
t.run(App())
Custom Components
AbstractComponent lets you build reusable widgets that map directly to the render tree. Subclass it as a dataclass and implement get_node() — return any RenderNode (paragraph, list, progress bar, table, etc.) and xnano handles the rest. Components slot into Grid fields like any other value.
import dataclasses
from xnano.beta import Grid, Field, Terminal, Context
from xnano.beta.color import tailwind_color, pydantic_color
from xnano.beta.hooks import on_keyboard
from xnano.beta.components.abstract import AbstractComponent, ComponentRenderContext
from xnano.beta.core.nodes import ParagraphNode, RenderNode
@dataclasses.dataclass
class Badge(AbstractComponent):
label: str = ""
color: str = "white"
def get_node(self, ctx: ComponentRenderContext) -> RenderNode:
return ParagraphNode(text=self.label, color=self.color)
class StatusBoard(Grid, direction="vertical", gap=1):
ok: Badge = Field(default_factory=lambda: Badge(label="● OK", color=tailwind_color("emerald", 500)), size=1)
warn: Badge = Field(default_factory=lambda: Badge(label="● Warning", color="yellow"), size=1)
err: Badge = Field(default_factory=lambda: Badge(label="● Error", color=pydantic_color("palevioletred")), size=1)
@on_keyboard("q")
def quit(self, ctx: Context) -> None:
ctx.terminal.request_exit()
Terminal().run(StatusBoard())
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 xnano-0.99.7.tar.gz.
File metadata
- Download URL: xnano-0.99.7.tar.gz
- Upload date:
- Size: 71.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62f55729d309e213875f98a8f545ad5b221973d8f800dbc19e1afcaa1413e86e
|
|
| MD5 |
436c952f0c1dbcdf5048a52bf1ff3b21
|
|
| BLAKE2b-256 |
09f7d2021baf2c36b0b6f067b99d5aa98c28eb7a2753c065c7756987ee48403c
|
File details
Details for the file xnano-0.99.7-py3-none-any.whl.
File metadata
- Download URL: xnano-0.99.7-py3-none-any.whl
- Upload date:
- Size: 84.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3a8fd549b3b0ccbccd61b6ebdd37c2e7346edb03f5e64e3c9a51d9326d69fff
|
|
| MD5 |
de11315362aa1f7705a53a6ce206e045
|
|
| BLAKE2b-256 |
0db1bed3fec77149e9f32785b8f55dfcfeb0f486a65e493608c795e6469069fb
|