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
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 | |
|---|---|---|---|
| richer_tui-0.1.0.tar.gz | 64.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| richer_tui-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
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
|