Skip to main content

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

  1. Color & Gradient
  2. Style & Layout
  3. LogKit -- structured logger
  4. Spinners
  5. Progress Bars
  6. Graphs
  7. Markup Language
  8. Syntax Highlighting
  9. Live & Dashboard
  10. Themes
  11. Screen Control
  12. Prompt
  13. Logging Bridge
  14. Text Object
  15. File Tree
  16. Markdown Renderer
  17. 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 or callable() -> str
  • refresh_rate -- redraws per second
  • transient=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)

Source distribution for richer-tui 0.1.0
File Size Uploaded
richer_tui-0.1.0.tar.gz 64.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for richer-tui 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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