xnano
An extremely nano terminal experience for Python, built on ratatui.
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')
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'))
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:
- Background & foreground colors (RGB, HEX, ANSI, Tailwind, ...)
- Text modifiers (bold, italic, underline, ...)
- Borders & border styling
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. (Requiresxnano-audio)Microphone: Allows capturing direct audio input from the system's microphone. (Requiresxnano-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')
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')
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'))
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')
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')
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')
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')
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')
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'))
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.