Skip to main content

xnano

An extremely nano terminal experience for Python, built on ratatui.

License Downloads Python Versions CodSpeed Built With Ratatui

Documentation · API Reference · Contribution Guide


xnano is a package for Python 3.10+ that aims to provide a complete terminal framework through an incredibly flexible and understandable API.

With xnano you can:

  • Render widgets, textual content, pixel art and even images in your terminal window.
  • Add interactive hooks that respond to user input and actions at runtime.
  • Compose, record & play audio with xnano-audio.

Installation

Install xnano from PYPI:

pip install xnano
# With image support
pip install "xnano[image]"
# With audio support
pip install "xnano[audio]"

Quick Start

import xnano

# Create a new canvas with `xnano.new()` & enable mouse events
# # Write content to the canvas with `canvas.write()`
canvas = xnano.new()
content = canvas.screen.write('Hello, world!', x='center', y='center')


# The canvas or any piece of content can register hooks.
# This hook is specific to the dimensions of the content itself!
@content.hook(on='mouse')
def change_color(ctx: xnano.Context):
    if ctx.mouse.button == 'left':
        content.color.fg = content.color.fg.random()


# now we can run our canvas in a live terminal
# `mouse=True` enables mouse events
with xnano.open(mouse=True) as terminal:
    terminal.screen.write(canvas)
    terminal.run(stop_on='q')
click-to-change-color

Core Features & Examples

Composing Content

The Canvas is a stateful builder class for composing a rectangular region within a terminal Screen. Canvases can be used to define anything from a simple header to a complex multi-window layout.

import xnano

# `layout='row'` (or `'column'`) shares a canvas's space between everything written into it
body = xnano.new(height=7, width='100%', layout='row')
body.screen.write(
    'left panel', border='plain', x='left', y='bottom', foreground=(255, 0, 0)
)
body.screen.write(
    'center panel', border='rounded', x='center', y='center', background='#f0f0f0'
)
body.screen.write('right panel', border='thick', x='right', y='top', foreground='blue')

# a canvas has access to all devices a terminal does
# you can render a static frame of its screen and print it directly!
print(body.screen.render(format='ansi'))
composing-content
Positioning & Sizing Content

xnano provides a collection of intuitive utilities and attributes that allow you to dynamically position, move and resize the content within your terminal window. Both audio and visual content follow a coordinate-based composition pattern.

Styling Content

All of ratatui's styling is natively supported within xnano. This includes:

Terminal and Terminal Devices

The Terminal class is the main entrypoint for rendering live visual and audio content. It provides a set of Devices that modularize the terminal's capabilities. These classes include:

  • Clock: The main requirement for any live terminal context. Allows controlling the tick, frame (FPS) and beat (BPM) rates for any live terminal context.
  • Screen: A 2D canvas of cells that can render content to the terminal.
  • Keyboard & Mouse: Allows capturing and emitting keyboard (key presses/clipboard paste) and mouse (click/drag/scroll) events.
  • Speaker: xnano's 'audio canvas' that provides a MIDI-based interface for composing and playing audio. (Requires xnano-audio)
  • Microphone: Allows capturing direct audio input from the system's microphone. (Requires xnano-audio)
Terminal
import xnano

# `xnano.open()` puts a live terminal on your screen: give it a `height` to draw in a
# region below your prompt, or leave it out to take over the whole window
with xnano.open(height=3, title='my app') as terminal:
    terminal.screen.write('Welcome to xnano!', border='rounded')
    label = terminal.screen.write('press space', x=22, y=1)

    # the terminal itself can register hooks, which hear every event on the screen
    @terminal.hook(on='key', keys='space')
    def pressed(ctx: xnano.Context):
        label.text = f'space at {ctx.clock.elapsed_seconds:.1f}s'

    # `run()` keeps the terminal alive until one of the `stop_on` keys is pressed
    terminal.run(stop_on='q')
terminal
Clock
import xnano

with xnano.open(height=3) as terminal:
    label = terminal.screen.write('')
    ball = terminal.screen.write('●', y=1)

    # the clock can be used directly in math, so the ball moves on every frame by itself
    ball.x = (terminal.clock * 10) % terminal.screen.width

    # tick hooks run on a timer, measured in seconds of the clock
    @terminal.hook(on='tick', every=1.0)
    def show_time(ctx: xnano.Context):
        label.text = f'{ctx.clock.elapsed_seconds:.0f}s on the clock'

    # `pause()` freezes everything that follows the clock, and `resume()` lets it run on
    @terminal.hook(on='key', keys='space')
    def toggle(ctx: xnano.Context):
        if ctx.clock.is_paused:
            ctx.clock.resume()
        else:
            ctx.clock.pause()
            label.text = 'paused: press space to resume'

    terminal.run(stop_on='q')
clock
Screen
import xnano

# each service's response time, and its requests over the last 30 seconds
services = {
    'api': ('41 ms', '▂▃▃▅▃▂▃▅▆▅▃▂▃▅▆▅▃▂▂▃▅▃▂▃▅▆▅▃▂▃'),
    'web': ('870 ms', '▃▅▆▅▃▂▂▃▅▇██▇▇█▇▅▆▇██▇▆▇█▇▆▅▆▇'),
    'worker': ('12 ms', '▁▁▂▁▁▃▁▁▁▂▅▂▁▁▁▂▁▁▃▂▁▁▁▂▁▁▁▂▃▁'),
}

# a canvas from `xnano.new()` needs no live terminal: `width='100%'` fills the window
with xnano.new(width='100%', height=7) as canvas:
    screen = canvas.screen

    # positions and sizes take cells, percentages or words such as 'center' and 'right'
    screen.write('services', bold=True)
    screen.write(f'{screen.width}×{screen.height} cells', x='right', y=0, dim=True)

    cards = {}
    for index, (name, (latency, graph)) in enumerate(services.items()):
        cards[name] = screen.write(
            ['● online', f'p95 {latency}', graph],
            x=f'{index * 34}%',
            y=1,
            width='32%',
            border='rounded',
            title=name,
            foreground='green',
        )

    # `write()` returns a live placement: its text and colors can change at any time
    cards['web'].text = cards['web'].text.replace('● online', '▲ slow responses')
    cards['web'].color.fg = 'yellow'

    # and it can be read back, like any other value
    healthy = sum(card.text.startswith('●') for card in cards.values())
    screen.write(f'{healthy} of {len(cards)} services healthy', y='bottom', dim=True)

    print(screen.render(format='ansi'))
screen
Keyboard
import xnano

# this stops any content within this terminal's canvas from moving outside it
with xnano.open(height=8, edges='stop') as terminal:
    player = terminal.screen.write('@', x='center', y='center')
    terminal.screen.write(
        'arrows or wasd to move · ctrl+r to reset · q to quit', y='bottom', dim=True
    )

    # `on='key'` hooks run on every key press, and again while a key repeats
    @terminal.hook(on='key')
    def move(ctx: xnano.Context):
        # `direction()` reads the arrow keys (or wasd) as a step along each axis
        step_x, step_y = ctx.keyboard.direction()
        player.move(step_x, step_y)

    # `keys=` narrows a hook to one key, or a binding with modifiers
    @terminal.hook(on='key', keys='ctrl+r')
    def reset(ctx: xnano.Context):
        player.x, player.y = 'center', 'center'

    terminal.run(stop_on='q')
keyboard
Mouse
import xnano

# `mouse=True` enables mouse events
with xnano.open(height=3, mouse=True) as terminal:
    terminal.screen.write('─' * 30, y=1)
    label = terminal.screen.write('drag the ●', y=2)

    # a range keeps the thumb's `x` between two columns, however far it's dragged
    thumb = terminal.screen.write('●', x=(0, 29), y=1)

    # a drag hook is a generator: each `yield` waits for the next movement of the pointer
    @thumb.hook(on='drag')
    def slide(ctx: xnano.Context):
        while ctx.mouse.is_down:
            thumb.x = ctx.parent.local(ctx.mouse.position).x
            ctx = yield
        label.text = f'dropped at {thumb.x}'

    terminal.run(stop_on='q')
mouse
Speaker
import xnano

# notes are plain values, so they can be defined once and played anywhere
scale = ['C4', 'D4', 'E4', 'G4', 'A4']
chord = xnano.Note(('C4', 'E4', 'G4'), beats=2, instrument='electric_piano')

with xnano.open(height=2) as terminal:
    terminal.screen.write('press 1-5 for a note, space for a chord')
    playing = terminal.screen.write('', y=1, foreground='cyan')

    @terminal.hook(on='key')
    def play_note(ctx: xnano.Context):
        key = ctx.keyboard.key
        if key in ('1', '2', '3', '4', '5'):
            pitch = scale[int(key) - 1]
            ctx.speaker.play(xnano.Note(pitch, beats=0.5))
            playing.text = f'♪ {pitch}'
        elif key == 'space':
            ctx.speaker.play(chord)
            playing.text = '♪ C4 E4 G4'

    # melodies can also be written as text: a pitch, and its length in beats after a `:`
    if terminal.speaker.is_available:
        terminal.speaker.play('C4 E4 G4:2', instrument='piano')
    terminal.run(stop_on='q')
speaker
Microphone
import xnano

with xnano.open(height=2) as terminal:
    terminal.screen.write('speak into your microphone')

    # `level` is an expression from 0.0 to 1.0, so content can follow it on every frame
    # (the default input device opens the first time a frame reads it)
    level = terminal.microphone.level
    terminal.screen.write('░' * 40, x=0, y=1, dim=True)
    terminal.screen.write(
        '█' * 40, x=0, y=1, width=level * 40 + 1, foreground='green', layer=1
    )

    terminal.run(stop_on='q')
microphone
Terminal Widgets _(from [Ratatui Widgets](https://ratatui.rs/concepts/widgets/)_

All of ratatui's standard widgets (along with a few extras) are built into xnano:

import math

import xnano
from xnano.styles import Style
from xnano.widgets import Bar, BarChart, Gauge, Sparkline, Table, TableCell, TableRow

# requests per second over the last 98 seconds
requests = [
    round(6 + 3 * math.sin(second / 5) + 2 * math.sin(second / 2)) for second in range(98)
]

with xnano.open(height=13) as terminal:
    # widgets are written (and sized) like any other content, borders and titles included
    terminal.screen.write(
        Gauge(0.6, label='60% of 512 GB', gauge_style=Style(foreground='green')),
        width='100%',
        border='rounded',
        title='disk',
    )
    terminal.screen.write(
        Sparkline(requests, style=Style(foreground='cyan')),
        width='100%',
        height=4,
        border='rounded',
        title='requests',
    )
    load = terminal.screen.write(
        BarChart(
            [
                Bar(73, label='cpu'),
                Bar(41, label='memory'),
                Bar(18, label='network'),
                Bar(56, label='disk io'),
            ],
            bar_width=8,
            bar_gap=3,
            bar_style=Style(foreground='magenta'),
        ),
        width='48%',
        height=6,
        border='rounded',
        title='load',
    )
    # a placement's position can be read back, so the table lines up with the chart
    terminal.screen.write(
        Table(
            [
                ['web', 'up', '41 ms'],
                ['api', 'up', '12 ms'],
                ['db', TableCell('down', style=Style(foreground='red')), '—'],
            ],
            header=TableRow(['service', 'status', 'p95'], style=Style(bold=True)),
        ),
        x='right',
        y=load.y,
        width='48%',
        height=6,
        border='rounded',
        title='services',
    )
    terminal.run(stop_on='q')
widgets
Images & Pixel Art

Pixel art is a first-class concept within the library, additionally complemented by the xnano-image package which provides native image rendering (thanks to ratatui-image).

Pixel Art

Images

You can render both static image content (.png, .jpg) as well as animated files such as .gif with no extra effort.

First, ensure you have the xnano-image package installed which can be added through one of the two commands:

pip install "xnano[image]"
pip install xnano-image

Now, you can simply load and directly print an image without even needing to open a live xnano terminal context.

import pathlib

import xnano

# load a `.png`, `.jpg` or animated `.gif`, sized in terminal cells or a percentage
# (this picture ships with xnano, so the example runs anywhere: use any path of your own)
picture = xnano.load_image(
    pathlib.Path(xnano.__file__).parent / 'assets' / 'dusk.png', width='100%'
)

# `xnano.region()` draws content into cells without a terminal, so it can be printed as is
print(xnano.region(picture).render(format='ansi'))
images
Audio

Metadata

Release files for xnano 2.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xnano 2.1.1
File Size Uploaded Provenance Actions
xnano-2.1.1.tar.gz 1.8 MB Without provenance Details for xnano-2.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for xnano 2.1.1
File Interpreter ABI Platform Actions
xnano-2.1.1-py3-none-any.whl Python 3 none any Details for xnano-2.1.1-py3-none-any.whl

Total release size: 2.6 MB

Release files / xnano-2.1.1.tar.gz

Download URL xnano-2.1.1.tar.gz
Size 1.8 MB
Tags Source
SHA-256 checksum
How to use checksums
2c152f0eb13eb5cb891b76446efe5d7275d376bc509936fb725f7f24ef9c3e29
BLAKE2b-256 checksum
How to use checksums
8e841a7c95a452778ff67b8544847708e192ed2bc31e72aad3ece94189cf6795
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","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}

Release files / xnano-2.1.1-py3-none-any.whl

Download URL xnano-2.1.1-py3-none-any.whl
Size 817.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2e5c7ae538d3d043af6a5cbf52041a2a208c4b8a910078b9d7ecdd17f532e6a2
BLAKE2b-256 checksum
How to use checksums
9dd6ae9458c403cadc6416ea16f5b4d111621505804e60cb5c47e42f79421113
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","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}

Security for xnano 2.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page