Skip to main content

xnano Title Animation

xnano

License Python Versions CodSpeed Built With Ratatui

A simple python tui framework built on top of the ratatui and tachyonfx rust libraries.

[!NOTE] Documentation with examples that are runnable & editable in-browser are now available at https://xnano.hammad.app!

xnano is a modern, lightweight 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

pip install "xnano>=1.2.3"

Or use uv:

uv add "xnano>=1.2.3"

[!TIP] Try running the python -m xnano or uv run xnano commands to test out the built in demo application.

uv run xnano

Examples

You can view the code for these examples here: examples.

xnano Agent Chat Demo

xnano Feed Demo

xnano Kanban Demo

Your first render

The easiest way to get started is the print-like render() helper — no session, no event loop. It writes styled content to the terminal and returns.

from xnano import render
from xnano.components import Text

render(
    Text("Hello from xnano!", foreground="violet", modifiers=["bold"])
)

Your first render

You can pass multiple renderables — they stack vertically:

from xnano import render
from xnano.components import Text

render(
    Text("● Done: ", foreground="emerald-400", modifiers=["bold"]),
    Text("All 12 checks passed.", foreground="slate-400"),
)

Multiple renderables


Styled text

Text composes rich inline content with colors, modifiers, and nesting:

from xnano import render
from xnano.components import Text

message = Text([
    Text("● ", foreground="emerald-400"),
    Text("Done: ", foreground="white", modifiers=["bold"]),
    Text("all tests passed\n", foreground="slate-300"),
])

render(message)

Styled text

Colors accept Tailwind names ("violet-500"), hex strings ("#a78bfa"), or plain names ("white", "red"). The deprecated color= alias still works at runtime and maps to foreground=.


Hello World

The minimal xnano app. Define a BaseGrid 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.

from xnano import BaseGrid, Field, Terminal, Context, on_tick, on_keyboard

class App(BaseGrid):
    message: str = Field(
        default="Hello, world!",
        foreground="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", foreground="white")
        else:
            self.current_color = "sky"
            self.grid_set_field(
                "message",
                foreground="sky-500",
            )

    @on_keyboard("q")
    def quit(self, ctx: Context) -> None:
        ctx.terminal.request_exit()

Terminal().run(App())

Hello World


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 BaseGrid inside another as a Field value. Direction ("horizontal" / "vertical") and gap control how fields are laid out. Use width / height (absolute cells, "50%", or "1fr") to proportion each slot.

from xnano import BaseGrid, Field, Terminal, Context, on_keyboard

class SidebarTitle(BaseGrid, horizontal_align="center"):
    title: str = Field("This is a title.", horizontal_align="center")

class Sidebar(BaseGrid, direction="vertical"):
    title: SidebarTitle = Field(
        default_factory=SidebarTitle,
        height="10%",
    )
    nav: str = Field(default="- Home", height="1fr")

class App(BaseGrid, direction="horizontal", gap=1):
    sidebar: Sidebar = Field(
        default_factory=Sidebar,
        width="25%",
    )
    content: str = Field(
        default="Main area",
        width="1fr",
        border="rounded",
    )

    @on_keyboard("q")
    def quit(self, ctx: Context) -> None:
        ctx.terminal.request_exit()

Terminal().run(App())

Layout & Nesting


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 import BaseGrid, Field, Terminal, Context, on_keyboard

class Counter(BaseGrid, direction="vertical", gap=1):
    label: str = Field(
        default="Count: 0",
        height=1,
        border="rounded",
        border_color="violet-500",
    )
    hint: str = Field(
        default="  ↑ / ↓ to count  ·  q to quit",
        height=1,
        foreground="slate-500",
    )
    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())

Keyboard Events


Click Handlers

Live terminal sessions enable mouse input by default. 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 import BaseGrid, Field, Terminal, Context, on_click, on_keyboard

class App(BaseGrid, direction="vertical", gap=1):
    button: str = Field(
        default="  [ Click me ]  ",
        height=3,
        border="rounded",
        border_color="violet-500",
    )
    status: str = Field(
        default="  Waiting...",
        height=1,
        foreground="slate-400",
    )

    @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().run(App())

Click Handlers


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.

import time
from xnano import BaseGrid, Field, Terminal, Context, on_tick, on_keyboard

class Clock(BaseGrid, direction="vertical", gap=1):
    time_display: str = Field(
        default="",
        height=3,
        border="rounded",
        border_color="teal-500",
        title=" Clock ",
    )
    hint: str = Field(
        default="  q to quit",
        height=1,
        foreground="slate-500",
    )

    def __post_init__(self) -> None:
        self.time_display = f"  {time.strftime('%H:%M:%S')}"

    @on_tick(1000)
    def update_time(self) -> None:
        self.time_display = f"  {time.strftime('%H:%M:%S')}"

    @on_keyboard("q")
    def quit(self, ctx: Context) -> None:
        ctx.terminal.request_exit()

Terminal(tick_interval=1000).run(Clock())

Timed Updates


State & Context Manager

Pass any object as state to Terminal to thread shared data through the session. Every BaseGrid 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 import BaseGrid, Field, Terminal, Context, on_keyboard

@dataclass
class AppState:
    username: str = "guest"

class App(BaseGrid, direction="vertical", gap=1):
    header: str = Field(
        default="",
        height=1,
        foreground="white",
        background="violet-900",
    )
    body: str = Field(
        default="Press q to quit",
        foreground="slate-400",
    )

    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())

State & Context Manager


Custom Components

Subclass Component and implement compose() to return interface-neutral content (for example a TextBlock, Panel, TableGrid, or Canvas). Components slot into BaseGrid fields like any other value.

import dataclasses
from xnano import BaseGrid, Field, Terminal, Context, on_keyboard
from xnano.components import Component, TextBlock

@dataclasses.dataclass
class Badge(Component):
    label: str = ""
    foreground: str = "white"

    def compose(self, ctx):
        return TextBlock.from_plain(
            self.label,
            color=self.foreground,
        )

class StatusBoard(BaseGrid, direction="vertical", gap=1):
    ok: Badge = Field(
        default_factory=lambda: Badge(
            label="● OK",
            foreground="emerald-500",
        ),
        height=1,
    )
    warn: Badge = Field(
        default_factory=lambda: Badge(
            label="● Warning",
            foreground="yellow",
        ),
        height=1,
    )
    err: Badge = Field(
        default_factory=lambda: Badge(
            label="● Error",
            foreground="palevioletred",
        ),
        height=1,
    )

    @on_keyboard("q")
    def quit(self, ctx: Context) -> None:
        ctx.terminal.request_exit()

Terminal().run(StatusBoard())

Custom Components

Download files

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

Source Distribution

xnano-1.2.3.tar.gz (201.8 kB view details)

Uploaded Source

Built Distribution

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

xnano-1.2.3-py3-none-any.whl (235.5 kB view details)

Uploaded Python 3

File details

Details for the file xnano-1.2.3.tar.gz.

File metadata

  • Download URL: xnano-1.2.3.tar.gz
  • Upload date:
  • Size: 201.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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

Hashes for xnano-1.2.3.tar.gz
Algorithm Hash digest
SHA256 fdec93c6d6d5e01420a0dadfc625665b3f79ff16c26602bbccdecb2a0b34599e
MD5 90de240bb96202023a2b51742c8dab86
BLAKE2b-256 fe64b754aaf16386bb8daf0f57efd92607e6d70b95d1354574ca01084a738a92

See more details on using hashes here.

File details

Details for the file xnano-1.2.3-py3-none-any.whl.

File metadata

  • Download URL: xnano-1.2.3-py3-none-any.whl
  • Upload date:
  • Size: 235.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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

Hashes for xnano-1.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 8e790b19b127568d362ba0b76119b97d9f3180018984d8218be201d001243cdc
MD5 46a6d96c3952b22046a59655993180f0
BLAKE2b-256 b7819bfc498ee117a8d588659d1541448c9ac5c30207249f152eab58db85896e

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.3

Supported by

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