richer
Zero-dependency Python TUI toolkit. True-color ANSI rendering, logging, progress bars, spinners, graphs, live dashboards, syntax highlighting, markdown, markup language, prompts, file trees, themes -- all from scratch, no rich, no curses, no pygments.
pip install richer-tui
python -m richer.demo # live preview of everything
Requires Python 3.10+, a terminal with true-color support (any modern terminal on Windows/macOS/Linux).
Table of Contents
- Color & Gradient
- Style & Layout
- LogKit -- structured logger
- Spinners
- Progress Bars
- Graphs
- Markup Language
- Syntax Highlighting
- Live & Dashboard
- Themes
- Screen Control
- Prompt
- Logging Bridge
- Text Object
- File Tree
- Markdown Renderer
- Loading Screen
1. Color & Gradient
from richer import Color, Gradient
Color -- ANSI escape helpers
# 16-color constants
Color.RED # "\033[31m"
Color.BRIGHT_WHITE # "\033[97m"
Color.BG_BLUE # "\033[44m"
Color.RESET # "\033[0m"
# True-color (24-bit)
Color.rgb(88, 101, 242) # foreground escape
Color.bg_rgb(30, 30, 40) # background escape
Color.from_hex("#5865F2") # hex string -> rgb escape
# 256-color
Color.color256(196)
Color.bg_color256(52)
# Gradient text
Color.gradient_text("hello", (255, 80, 0), (255, 220, 0))
# Multi-stop gradient -- stops are (r,g,b) tuples
Color.multi_gradient("loading...", (88,101,242), (0,200,255), (0,255,150))
# Full HSV rainbow rotation
Color.rainbow("spectral")
# Utility
Color.blend((255,0,0), (0,0,255), 0.5) # lerp -> (127, 0, 127)
Color.pulse((88,101,242), t) # sinusoidal brightness, t in [0,1]
Color.dim_rgb((200,200,200), factor=0.4) # darken tuple -> rgb escape
Color.strip("text with \033[31mcodes\033[0m") # strip all ANSI
# Convenience wrappers
Color.bold("text")
Color.italic("text")
Color.underline("text")
Color.strike("text")
Color.dim_text("text")
Gradient -- named stop lists
Every preset is a list[tuple[int,int,int]] passed to Color.multi_gradient.
| Name | Visual |
|---|---|
FIRE |
red -> orange -> yellow |
OCEAN |
deep navy -> cyan |
NEON |
pink -> violet -> cyan |
MATRIX |
dark green -> bright green |
GOLD |
amber -> bright gold -> amber |
BLOOD |
dark red -> bright red |
CYBER |
teal -> blue -> violet |
DISCORD |
discord blurple loop |
CANDY |
pink -> yellow -> light blue |
TOXIC |
lime -> yellow-green |
VOID |
deep purple -> violet |
LAVA |
red -> orange -> yellow |
SUNSET |
orange -> crimson -> indigo |
ARCTIC |
ice blue -> white-blue |
RUST |
dark brown -> orange |
ACID |
lime -> bright green |
GHOST |
cool grey loop |
INFRA |
deep blue -> purple -> red -> yellow |
ROYAL |
purple -> lavender loop |
# Build from hex strings
stops = Gradient.from_hex("#ff0080", "#8000ff", "#00c8ff")
# Expand stop list to n evenly-spaced (r,g,b) tuples
pts = Gradient.interpolate(Gradient.FIRE, 256)
2. Style & Layout
from richer import Style, BorderStyle, render_panel, render_table, render_two_col, render_header, render_badge, strip_ansi, term_width
Style -- ANSI text attributes
Style.BOLD, Style.DIM, Style.ITALIC, Style.UNDERLINE, Style.BLINK, Style.STRIKE, Style.RESET
BorderStyle -- box-drawing presets
SINGLE, DOUBLE, ROUNDED, BOLD, DASHED, ASCII
Layout functions
# Bordered panel -- returns string
render_panel(
lines, # list[str]
title=None,
border=BorderStyle.ROUNDED,
border_color=Color.rgb(88,101,242),
title_color=Color.BRIGHT_WHITE,
padding=1,
width=None,
)
# Table -- returns string
render_table(
headers, # list[str]
rows, # list[list[str]]
border=BorderStyle.SINGLE,
header_color=...,
border_color=...,
col_colors=None, # list[str] per column
zebra=False,
align=None, # list["left"|"right"|"center"]
)
# Two-column layout -- returns string
render_two_col(left_lines, right_lines, gap=4)
# Centered header bar -- returns string
render_header(text, gradient=None, width=None)
# Small badge pill -- returns string
render_badge(text, color=None)
# Strip ANSI escapes
strip_ansi(s)
# Terminal width (falls back to 80)
term_width()
3. LogKit -- structured logger
from richer import LogKit, Level
log = LogKit(
name="app",
min_level=Level.DEBUG,
show_time=True,
show_name=True,
name_gradient=Gradient.DISCORD,
time_color=Color.BRIGHT_BLACK,
file=sys.stdout,
thread_safe=False,
history=False,
history_maxlen=500,
)
Log levels
log.debug("probe sent")
log.info("handshake complete")
log.success("payload injected")
log.warning("retry 3/5")
log.error("connection refused")
log.critical("watchdog timeout")
# Custom color override
log.info("custom", color=Color.rgb(200, 100, 255))
Level constants: DEBUG=0, INFO=1, SUCCESS=2, WARNING=3, ERROR=4, CRITICAL=5
Display methods
log.banner("SYSTEM COMPROMISED", gradient=Gradient.BLOOD, border=BorderStyle.DOUBLE)
log.rule("section header", gradient=Gradient.NEON)
log.rule()
log.divider()
log.section("phase 2", gradient=Gradient.CYBER)
log.panel(["line 1", "line 2"], title="status", border=BorderStyle.ROUNDED)
log.table(
["host", "status", "ip"],
[["web01", "up", "10.0.0.1"], ["db02", "down", "10.0.0.2"]],
zebra=True,
)
log.kv([("pid", "1234"), ("arch", "x64")], sep=" -> ")
log.keyval("target", "192.168.1.100")
log.columns(["item1","item2","item3","item4"], ncols=2, headers=["col A","col B"])
log.callout("Heap base leaked at 0x7fff00001000", style="note")
# styles: "note", "tip", "warn", "danger", "info"
log.badge("STABLE", color=Color.rgb(50,220,100))
Data inspection
log.tree({"a": {"b": [1, 2, 3]}, "c": "val"})
log.json({"status": "ok", "code": 200, "data": None})
log.hex_dump(bytes(range(64)), width=16, highlight=[0x00, 0xff])
log.diff(old_str, new_str, label_a="before", label_b="after")
log.inspect(obj, show_private=False, show_methods=False)
log.traceback() # current exception
log.traceback(exc) # specific exception object
log.multiline(["line1", "line2"], level=Level.INFO)
Progress & metrics
# Inline progress bar (overwrites current line)
for i in range(total + 1):
log.progress_bar(i, total, label="downloading", show_eta=True, start_time=t0)
# Inline counter
log.counter("packets", current, total, bar=True, bar_width=20)
# Status line (overwritten by next log call)
log.status("waiting for beacon...")
log.status_clear()
# Sparkline
log.sparkline([10, 25, 18, 40, 35, 55], label="rtt", gradient=Gradient.NEON)
# Rate and elapsed
log.rate("req", count=1024, elapsed=2.5, unit="/s")
log.elapsed(start_time, label="total time")
Integration methods
log.syntax(code, lang="python", line_numbers=True, title="exploit.py")
log.markdown(md_text)
log.markup("[fire]armed[/fire] and [bold green]ready[/bold green]")
log.file_tree("./src", max_depth=3, show_size=True)
log.theme("dracula")
History
log = LogKit("app", history=True)
log.info("boot")
records = log._hist.all() # [{"level": 1, "msg": "boot", "ts": "..."}]
log._hist.clear()
4. Spinners
from richer import Spinner, SpinnerGroup, SPINNER_FRAMES
Spinner
s = Spinner("connecting to C2", style="dots")
s.start()
# ... work ...
s.stop("connected")
# Context manager
with Spinner("encrypting payload"):
encrypt(data)
SpinnerGroup
sg = SpinnerGroup([
"stage 1 -- recon",
"stage 2 -- exploit",
"stage 3 -- persist",
])
sg.start()
time.sleep(1)
sg.done(0)
time.sleep(1)
sg.done(1)
sg.stop()
5. Progress Bars
from richer import ProgressBar, MultiBar, ThreadedBar, RateBar, BAR_STYLES
ProgressBar
b = ProgressBar(
total=100,
width=40,
style="block",
fill_gradient=Gradient.FIRE,
prefix="download ",
show_count=True,
show_pct=True,
)
for i in range(101):
b.update(i)
time.sleep(0.02)
print()
# Iterable wrapper
for item in b(my_list):
process(item)
MultiBar
mb = MultiBar([
("loader", 40, Gradient.DISCORD),
("encoder", 60, Gradient.MATRIX),
("packer", 30, Gradient.FIRE),
], width=35)
vals = [0, 0, 0]
for i in range(60):
vals[0] = min(i + 1, 40)
vals[1] = i + 1
vals[2] = min(i + 1, 30)
mb.render_all(vals)
time.sleep(0.03)
print()
ThreadedBar
tb = ThreadedBar(total=100, interval=0.05, prefix="upload ", fill_gradient=Gradient.OCEAN)
tb.start()
for chunk in chunks:
upload(chunk)
tb.inc()
tb.stop()
6. Graphs
from richer import (
bar_chart, sparkline, heatmap_row, column_chart,
line_graph, scatter, gauge, timeline,
stacked_bar_chart, pie_chart,
)
All graph functions return strings -- print() them or embed in panels.
# Horizontal bar chart
print(bar_chart(
[92, 78, 45, 61],
labels=["RCE", "LPE", "SQLi", "XSS"],
title="severity",
gradient=Gradient.BLOOD,
width=35,
))
# Inline sparkline
s = sparkline([10, 25, 18, 40, 35, 55], gradient=Gradient.NEON)
# Heatmap row
print(heatmap_row([0.1, 0.5, 0.9, 0.3], labels=["M","T","W","T"]))
# Vertical column chart
print(column_chart([30, 80, 50, 90], labels=["Q1","Q2","Q3","Q4"], height=10))
# Multi-series line graph
# NOTE: colors must be (r,g,b) tuples, NOT ANSI escape strings
print(line_graph(
[[10,25,18,40], [5,15,30,20]],
labels=["in", "out"],
colors=[(88,101,242), (237,66,69)],
height=8, width=50,
))
# Scatter plot
pts = [(x, y), ...]
print(scatter(pts, width=50, height=16, gradient=Gradient.NEON))
# Radial gauge
print(gauge(0.73, label="CPU", gradient=Gradient.LAVA))
# Timeline
events = [("boot", 0), ("exploit", 3), ("shell", 7), ("exfil", 12)]
print(timeline(events, width=60))
# Stacked bar chart
print(stacked_bar_chart(
series=[[30,50,20], [40,35,25]],
labels=["host1", "host2"],
series_labels=["user", "kernel", "idle"],
colors=[(88,101,242),(59,165,93),(250,166,26)],
title="CPU breakdown",
width=40,
))
# Pie chart
print(pie_chart(
data=[40, 30, 20, 10],
labels=["RCE", "LPE", "DoS", "Info"],
title="vuln classes",
show_pct=True,
))
7. Markup Language
from richer import markup_render, markup_strip, mprint, mformat
Tags
[red]text[/red] [green] [blue] [yellow] [cyan] [magenta] [white] [dim]
[bold]text[/bold] [italic] [underline] [strike] [blink]
[rgb(88,101,242)]text[/rgb]
[#5865f2]text[/#5865f2]
[bg_rgb(30,30,40)]text[/bg_rgb]
# Combined
[bold red]critical alert[/bold red]
# Named gradients
[fire]armed[/fire] [neon] [matrix] [ocean] [blood] [cyber] [discord] [candy]
[rainbow]full spectrum[/rainbow]
# Hyperlink
[link=https://example.com]click here[/link]
s = markup_render("[bold red]ALERT[/bold red]: [neon]payload staged[/neon]")
plain = markup_strip("[bold]text[/bold]") # -> "text"
mprint("[fire]system compromised[/fire]")
mprint(mformat("[bold]{host}[/bold] responded in {ms}ms", host="10.0.0.1", ms=42))
8. Syntax Highlighting
from richer import highlight, highlight_auto, detect_lang
Regex-based tokenizer, zero external deps.
Supported languages: python, json, c, cpp, js, javascript, sh, bash, shell, yaml, sql, html, css, rust, go
code = "def exploit(buf): return buf[:8] + p64(0xdeadbeef)"
print(highlight(code, lang="python", line_numbers=True))
print(highlight_auto(code, filename="exploit.py", line_numbers=True))
lang = detect_lang(code, filename="main.rs") # -> "rust"
9. Live & Dashboard
from richer import Live, Dashboard, LogPane
Live
data = {"count": 0}
def render():
return f"packets: {data['count']}"
with Live(render, refresh_rate=10, transient=False):
for i in range(100):
data["count"] = i
time.sleep(0.05)
content-- string orcallable() -> strrefresh_rate-- redraws per secondtransient=True-- clears the block on exit
Dashboard
dash = Dashboard()
dash.row("header", height=3)
dash.cols(["left", "right"], heights=[20, 20])
dash.row("footer", height=1)
dash["header"] = render_panel(["STATUS BOARD"], border=BorderStyle.DOUBLE)
dash["left"] = "cell content left"
dash["right"] = "cell content right"
with Live(dash.render, refresh_rate=4):
for t in range(50):
dash["footer"] = f"tick {t}"
time.sleep(0.1)
LogPane
pane = LogPane(height=10)
pane.push("[OK] boot complete")
pane.push("[ERR] write failed")
with Live(pane.render, refresh_rate=5):
for event in event_stream():
pane.push(format_event(event))
time.sleep(0.05)
10. Themes
from richer import theme_apply, theme_color, theme_names, theme_info
from richer import DISCORD, HACKER, DRACULA, NORD, MONOKAI, SOLARIZED, CYBERPUNK, BLOOD
Available themes: discord, hacker, dracula, nord, monokai, solarized, cyberpunk, blood
theme_apply("dracula")
# Semantic color keys: "primary", "secondary", "success", "warning",
# "error", "info", "dim", "bright", "bg" (tuple), "gradient" (stop list)
c = theme_color("error") # ANSI escape string
bg = theme_color("bg") # (r,g,b) tuple
grad = theme_color("gradient") # gradient stop list
names = theme_names()
info = theme_info("monokai")
info = theme_info() # active theme
11. Screen Control
from richer import Screen, ScreenWriter
Screen.move(row, col)
Screen.clear()
Screen.hide_cursor()
Screen.show_cursor()
Screen.set_title("my app")
Screen.bell()
w, h = Screen.size()
Screen.draw_box(row, col, width, height, border=BorderStyle.SINGLE)
Screen.fill_rect(row, col, width, height, char=" ", color=None)
Screen.hyperlink(text, url)
Screen.notify(title, body)
# Context managers
with Screen.fullscreen:
...
with Screen.cursor_hidden:
...
with Screen.at(5, 10):
print("drawn at row 5 col 10")
sw = ScreenWriter()
sw.at(3, 5, f"{Color.rgb(88,101,242)}hello{Color.RESET}")
sw.flush()
12. Prompt
from richer import Prompt
name = Prompt.ask("target hostname", default="localhost", hint="e.g. 10.0.0.1")
confirmed = Prompt.confirm("continue?", default=True)
choice = Prompt.choose("select payload", choices=["revshell", "bind", "meter"])
selected = Prompt.choose("select modules", choices=[...], multi=True)
key = Prompt.secret("encryption key", confirm=True)
port = Prompt.integer("port", min_val=1, max_val=65535, default=4444)
timeout = Prompt.float_("timeout seconds", default=5.0)
path = Prompt.path("config file", must_exist=True)
13. Logging Bridge
from richer import LogKitHandler, setup_logging, suppress_loggers
import logging
log = LogKit("app")
# Route all logging.* calls through log
setup_logging(log, level=logging.DEBUG, capture_warnings=True)
# Specific loggers only
setup_logging(log, loggers=["httpx", "asyncio"])
# Silence noisy loggers
suppress_loggers("urllib3", "PIL")
14. Text Object
from richer import Text
t = Text()
t.append("status: ", "bold")
t.append("ONLINE", "rgb(50,220,100)")
t.append_markup("[fire]armed[/fire]")
print(t.render()) # ANSI string
print(t.plain) # stripped text
print(t.width) # visual width
t.truncate(40, overflow="...")
lines = t.wrap(60)
t.justify("center", 80)
t.highlight_substr("ONLINE", "bold yellow")
# Class methods
t = Text.assemble(("label: ", "dim"), ("value", "bold green"))
t = Text.from_markup("[bold red]ALERT[/bold red]")
t = Text.gradient("spectral", (255,0,128), (0,200,255))
t = Text.rule(title="section", width=60, char="-")
15. File Tree
from richer import render_file_tree, print_file_tree
tree_str = render_file_tree(
path="./src",
max_depth=4,
show_hidden=False,
show_size=True,
dir_only=False,
sort_dirs_first=True,
summary=True,
)
print_file_tree("./src", max_depth=3, show_size=True)
60+ extension color mappings: .py (blue), .rs (orange), .c/.cpp (cyan), .sh (green), .json/.yaml (yellow), .md (white), .env (red), images (magenta), binaries (bright red), and more.
16. Markdown Renderer
from richer import markdown_render, md_print
GFM renderer, zero deps.
Supported: headings h1-h6, bold/italic/strikethrough, inline code, links, blockquotes, unordered/ordered lists (3 nesting levels), GFM tables with alignment, fenced code blocks (syntax highlighted), horizontal rules.
md = """
# Exploitation Report
Target responded to **CVE-2024-XXXX** with a *9.8 CVSS* score.
```python
payload = b"\\x00" * 8 + p64(win_addr)
sock.send(payload)
| Component | Status | Severity |
|---|---|---|
| web | pwned | critical |
| """ |
md_print(md) s = markdown_render(md) # -> ANSI string
---
## 17. Loading Screen
```python
from richer import loading_screen, G_BOOT, G_READY, G_PULSE
loading_screen(
title="IMPLANT LOADER v2.0",
steps=[
"initializing crypto",
"probing target",
"establishing tunnel",
"loading modules",
],
gradient=G_BOOT,
step_delay=0.8,
final_msg="ready",
)
Quick-start
from richer import LogKit, Color, Gradient, Spinner, ProgressBar, bar_chart, mprint, theme_apply
import time
theme_apply("dracula")
log = LogKit("demo", show_time=True)
log.banner("RICHER DEMO", gradient=Gradient.NEON)
log.info("starting up")
log.success("connected to 10.0.0.1:4444")
log.warning("retry 2/5 -- timeout")
log.error("module load failed: access denied")
log.kv([
("pid", "31337"),
("arch", "x64"),
("priv", "SYSTEM"),
("os", "Windows 11 22H2"),
])
with Spinner("staging payload", style="dots"):
time.sleep(1.5)
b = ProgressBar(total=100, width=35, fill_gradient=Gradient.FIRE, prefix="upload ")
for i in range(101):
b.update(i)
time.sleep(0.01)
print()
print(bar_chart([92,78,45,61], labels=["RCE","LPE","SQLi","XSS"], gradient=Gradient.BLOOD))
mprint("[fire]armed[/fire] and [bold green]ready[/bold green]")
Project layout
richer/
|-- __init__.py exports
|-- colors.py Color, Gradient
|-- styles.py Style, BorderStyle, layout helpers
|-- logger.py LogKit, Level
|-- spinners.py Spinner, SpinnerGroup
|-- bars.py ProgressBar, MultiBar, ThreadedBar, RateBar
|-- graphs.py bar_chart, line_graph, scatter, pie_chart, ...
|-- markup.py tag markup language
|-- syntax.py regex-based syntax highlighter
|-- live.py Live, Dashboard, LogPane
|-- themes.py 8 named themes
|-- screen.py Screen, ScreenWriter
|-- prompt.py Prompt.*
|-- loghandler.py stdlib logging bridge
|-- text.py Text object
|-- filetree.py render_file_tree
|-- markdown.py GFM markdown renderer
|-- loadscreen.py animated boot screen
examples/
|-- demo.py runnable feature showcase
License
MIT
Metadata
Release files for richer-tui 0.1.0
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 |
|---|---|---|---|---|
| richer_tui-0.1.0.tar.gz | 64.2 kB | Without provenance | Details for richer_tui-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| richer_tui-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for richer_tui-0.1.0-py3-none-any.whl |
Total release size: 124.9 kB
Release files / richer_tui-0.1.0.tar.gz
| Download URL | richer_tui-0.1.0.tar.gz |
|---|---|
| Size | 64.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dad4068c4f21d0b03dbff07e857de041f9650543e4423f9d48a0ac1d67925636
|
|
BLAKE2b-256 checksum How to use checksums |
42f2efab988bbf4a931e2cec641c10f5ab100b3bc70a1a26d55864caac3ce85b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / richer_tui-0.1.0-py3-none-any.whl
| Download URL | richer_tui-0.1.0-py3-none-any.whl |
|---|---|
| Size | 60.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5c6a91fc3c7da1f1308d11d5ead0dc9bcaf0c83c1201f18ebc4e7b2625efd5e9
|
|
BLAKE2b-256 checksum How to use checksums |
ffe8d7447c557b58b7d50bf1341f668145bbf90dd02fc005c03b60785624f158
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for richer-tui 0.1.0
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.