Skip to main content

painted

One library. Print to TUI. One dependency.

from painted import paint

paint({"cpu": 67, "mem": 82, "disk": 45})

TTY gets styled output. Pipe gets plain text. Same data, same function — paint() transcribes what the value declares, and it's the same verb every layer up the stack renders through.

Enter anywhere

Every entry point uses the same building blocks. Pick the one that fits your problem — you never hand over control, and there's no cliff between them.

Print styled output

Replace print() one call at a time. Auto-detects TTY — no ANSI garbage in pipes.

from painted import Block, Style, print_block

block = Block.text("deploy OK", Style(fg="green", bold=True))
print_block(block)

Compose

Blocks are immutable rectangles. Compose them with functions — no widget tree, no DOM.

from painted import border, join_vertical, pad, ROUNDED

header = Block.text(" api-gateway ", Style(bold=True, reverse=True))
status = join_vertical(
    Block.text("  replicas: 2/3 ready", Style(fg="yellow")),
    Block.text("  /health:  200  12ms", Style(fg="green")),
)
card = border(join_vertical(header, status), chars=ROUNDED)
print_block(card)

CLI harness

One render function, three output modes. Pipe gets static, TTY gets live updates, -i gets full interactive.

from painted import run_cli, CliContext, Block

def render(ctx: CliContext, data: dict) -> Block:
    # your render logic — returns a Block
    ...

def fetch() -> dict:
    return {"status": "ok", "replicas": 3}

run_cli(sys.argv[1:], render=render, fetch=fetch)
myapp              # auto-detect
myapp -q           # quiet (zoom 0)
myapp -v           # verbose (zoom 2)
myapp --json       # JSON output
myapp | grep ok    # plain text, no ANSI

The flag surface grows only as you declare capabilities — a flag exists only because the app declared it, and a declared capability must change output:

run_cli(
    sys.argv[1:], render=render, fetch=fetch,
    tags=[Tag("thinking", "Show reasoning", implied_at=3)],  # generates --thinking
    depth_aliases={"brief": 0, "full": 3},                   # --brief / --full
    budgets=True,                                            # --max-chars / --max-lines
)

Gate content with ctx.fidelity.shows("thinking"); read ctx.fidelity.chars for budgets. Add fetch_stream= for live updates (--live appears), and live_delivery="surface" to upgrade sustained streams to an alt-screen render loop (-i appears, converging with --live). Each rung is additive — climbing never rewrites the rung below. For multi-command apps, run_app routes subcommands through the same harness. The full consumer guide lives in src/painted/README.md.

Full TUI

Alt screen, keyboard input, async render loop, diff-flush. Subclass Surface, override render() and on_key().

import asyncio
from painted import Block, Style, border
from painted.tui import Surface

class MyApp(Surface):
    def render(self):
        block = Block.text("Hello!", Style(fg="green"))
        border(block, title="Demo").paint(self._buf)

    def on_key(self, key: str):
        if key == "q":
            self.quit()

asyncio.run(MyApp().run())

Install

pip install painted

One dependency: wcwidth (wide character display width).

API

Two stability tiers: painted.core + painted.views + painted.display are semver-stable (removing or renaming a public name is a major version — show()'s removal at 1.0 is that pre-declared event); painted.cli + painted.tui are the evolving framework surface and may change across minor versions — pre-1.0, pin accordingly.

Primitives

Export Purpose
Cell / Style Atomic display unit (char + style, frozen)
Span / Line Styled text with display-width awareness
Block Immutable rectangle of cells for composition

Composition

Export Purpose
join_horizontal / join_vertical Combine Blocks
pad / border / truncate Transform Blocks
BorderChars ROUNDED, HEAVY, DOUBLE, LIGHT, ASCII presets

Display

Export Purpose
paint(data) Zero-config display; transcribes any value onto the surface
print_block(block) Print a Block to stdout (TTY-aware)
run_cli(args, render=, fetch=, ...) CLI harness: zoom/mode/format, plus declared tags=, depth_aliases=, budgets=, fetch_stream=, live_delivery=
run_app(argv, commands) Multi-command routing; each AppCommand handler calls run_cli

Views (painted.views)

Export Purpose
shape_lens Auto-dispatch for exploration (numeric → chart, hierarchical → tree)
tree_lens / chart_lens Explicit tree and chart strategies
list_view / table / text_input Stateful interactive components
spinner / progress_bar / sparkline Animation and data viz

TUI (painted.tui)

Export Purpose
Surface Alt screen, keyboard, resize, diff-flush render loop
Layer Modal stack: Stay / Pop / Push / Quit
Buffer / BufferView 2D cell grid with region clipping

Aesthetic

Export Purpose
Palette 5 semantic Style roles (success, warning, error, accent, muted) + a series categorical ramp
IconSet Glyph vocabulary with ASCII fallback

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

painted-0.8.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

painted-0.8.0-py3-none-any.whl (429.5 kB view details)

Uploaded Python 3

File details

Details for the file painted-0.8.0.tar.gz.

File metadata

  • Download URL: painted-0.8.0.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for painted-0.8.0.tar.gz
Algorithm Hash digest
SHA256 92e7444a76f66e4a51876c380c4d12f1bf67af72b3b521a94cbebe8a693e530b
MD5 0fd1a5e86c2476d78b829c8cc9cce1ad
BLAKE2b-256 38d9a11cfb7fd9368863a3902f46faa0274a649459e63c981572517269c5ad0b

See more details on using hashes here.

Provenance

The following attestation bundles were made for painted-0.8.0.tar.gz:

Publisher: release.yml on kgruel/painted

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file painted-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: painted-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 429.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for painted-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a91c4e03cdfa7838f0efb232770c88308fc229a4569a90226eec4beeb7c53ffa
MD5 dffaf71e8237308e62a9cd3acf93b809
BLAKE2b-256 504e216c6c7093b4aef1ce12814925e779079b76ec35ca8fa4d74c06ef929637

See more details on using hashes here.

Provenance

The following attestation bundles were made for painted-0.8.0-py3-none-any.whl:

Publisher: release.yml on kgruel/painted

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.14.0

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

This release

0.8.0 This release

2 files

0.7.0

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.8

2 files

0.1.7

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page