Markup-based terminal coloring engine
Project description
tinting
Markup-based terminal coloring engine.
tinting converts @COLOR[ ... ]@ markup tokens into ANSI 256-color escape
sequences, so you can style terminal output without scattering raw escape
codes through your strings. It supports nested foreground/background colors,
bold/italic/underline modifiers, token escaping, visible-length measurement,
markup-aware slicing, and a grid-oriented style engine for terminal UIs.
- Pure Python, zero dependencies
- 32-color curated palette using 256-color codes
- Nestable markup with automatic style-stack restoration
- Respects
NO_COLOR,FORCE_COLOR, andCIenvironment variables - Advanced grid renderer for efficient per-cell TUI styling
Installation
pip install tinting
Requires Python 3.9+.
Quick start
from tinting import tprint
tprint("@GRN[success]@ @YEL#[ warning ]@ @*RED[bold red]@")
from tinting import ttext, tlen
styled = ttext("@BLU[hello]@ @RED[world]@")
print(tlen("@BLU[hello]@ @RED[world]@")) # 11 (markup ignored)
Markup syntax
Styling is expressed with opening/closing tokens that wrap the text to be
styled. An opening token is @ followed by optional modifiers, a color spec,
and [; the matching close is ]@.
| Token | Meaning |
|---|---|
@COLOR[text]@ |
Foreground COLOR |
@BG#FG[text]@ |
Background BG and foreground FG |
@BG#[text]@ |
Background BG only (foreground unchanged) |
@*COLOR[text]@ |
Bold |
@/COLOR[text]@ |
Italic |
@_COLOR[text]@ |
Underline |
@*/_COLOR[text]@ |
Combined modifiers (any order of * / _) |
@*BG#FG[text]@ |
Modifiers + background/foreground |
@|COLOR[text]|@ |
Escaped (rendered literally as @COLOR[text]@) |
Modifiers
The characters * (bold), / (italic), and _ (underline) may appear in any
order immediately after the leading @ and before the color spec. They can be
combined freely:
@*/BLU[bold italic blue]@
@_*/BLU[bold italic underlined blue]@
Background and foreground
Use # to separate background from foreground inside a color spec. Either
side may be omitted:
@NVY#WHT[white text on navy]@
@NVY#[navy background only]@
Nesting
Tags nest correctly: closing a tag restores the previously active style rather than resetting everything to default.
@GRN[green @RED[red]@ green again]@
Escaping
To render markup literally (so it is not interpreted as styling), use the
pipe-escaped forms @|COLOR[ and ]|@:
@|GRN[not green]|@ -> @GRN[not green]@
See tesc() for a helper that escapes active tokens programmatically.
Color palette
tinting ships a curated 32-color palette referenced by short three-letter
codes. Each color has a human-readable name and a 256-color code.
| Code | Name | 256 | Code | Name | 256 | |
|---|---|---|---|---|---|---|
| SLV | silver | 247 | LME | lime | 142 | |
| GRY | gray | 240 | OLV | olive | 58 | |
| LGR | lightgray | 252 | GRN | green | 64 | |
| BLK | black | 232 | FGN | forestgreen | 22 | |
| BGE | beige | 223 | TRQ | turquoise | 45 | |
| PNK | pink | 174 | CYN | cyan | 73 | |
| CRL | coral | 210 | TEA | teal | 23 | |
| RED | red | 124 | BLU | blue | 60 | |
| CRM | crimson | 88 | NVY | navy | 17 | |
| MRN | maroon | 1 | IND | indigo | 63 | |
| DRD | darkred | 52 | VIO | violet | 99 | |
| YEL | yellow | 172 | LAV | lavender | 183 | |
| ORG | orange | 130 | PRP | purple | 90 | |
| BRN | brown | 94 | PLM | plum | 96 | |
| WHT | white | 230 | MAG | magenta | 89 | |
| GLD | gold | 178 | RNB | rainbow | 201 |
The full mapping is available at runtime as Tinting._colors
(code -> (name, 256_code)), and Tinting.by_name (name -> code).
API reference
ttext(text, colors=True)
Convert tinting markup into ANSI escape sequences.
Parses @COLOR[ ... ]@ tokens (optionally combined with * for bold, / for
italic, _ for underline, and BG#FG for background plus foreground) and
replaces them with the corresponding ANSI escape codes while tracking a style
stack so that closing a tag restores the previously active style.
- Args:
text(str): The markup text to convert.colors(bool): WhenFalse, markup tokens are stripped without emitting ANSI codes, yielding plain text. Defaults toTrue.
- Returns:
str— the converted string wrapped in reset sequences when colors are enabled, otherwise the plain text.
ttext("@GRN[ok]@") # "\033[0m\033[38;5;64m...ok\033[39m...\033[0m"
ttext("@GRN[ok]@", False) # "ok"
tlen(text)
Measure the visible length of a styled string.
Counts only printable characters, ignoring all markup tokens. Escaped tokens
(@|... / ]|@) are counted as their literal characters.
- Args:
text(str) — the styled text to measure. - Returns:
int— the number of printable characters.
tlen("@GRN[hello]@") # 5
tlen("@GRN[a @RED[b]@ c]@") # 5 (a, space, b, space, c)
tlen("@|GRN[esc]|@") # 10 (the literal "@GRN[esc]@")
tesc(text, clean=False)
Escape tinting markup tokens so they are rendered literally.
Walks the text and rewrites active opening tokens (@COLOR[) to @|COLOR[
and closing tokens (]@) to ]|@, so a subsequent ttext() call emits them
as plain text. Already-escaped tokens are left untouched.
- Args:
text(str): The text whose active markup tokens should be neutralized.clean(bool): WhenTrue, escaped markers use%characters (%COLOR[/]%) instead of the pipe-escaped variants, producing plain readable text without pipes. Defaults toFalse.
- Returns:
str— the text with its markup tokens escaped.
tesc("@GRN[ok]@") # "@|GRN[ok]|@"
tesc("@GRN[ok]@", clean=True) # "%GRN[ok]%"
tslice(text, end, start=0)
Slice a styled string by visible character count.
Only printable characters count toward the slice bounds; markup tokens are
preserved so the styling context remains valid. The end bound is exclusive
(matching Python slice semantics). Escaped tokens are treated as literal text.
- Args:
text(str): The styled text to slice.end(int): The visible character index at which the slice ends (exclusive). Negative values are clamped to0.start(int): The visible character index at which the slice starts. A negative value is treated as its absolute value. Defaults to0.
- Returns:
str— the substring covering the requested visible range, with markup tokens retained.
tslice("hello world", 5) # "hello"
tslice("hello world", 11, 6) # "world"
tslice("@GRN[hello]@", 2) # "@GRN[he]@" (2 visible chars, markup kept)
tslice("@GRN[hello]@", 0) # "@GRN[]@" (empty content)
tprint(*args, **kwargs)
Print styled text to standard output.
Joins args with spaces, converts the result to ANSI escape sequences via
ttext() (honoring Tinting.colors), and forwards to the builtin print.
- Args:
*args: Values joined with spaces and converted to styled text.**kwargs: Additional keyword arguments forwarded toprint(e.g.end,sep,file).
tprint("@GRN[done]@", "@RED[check]@")
tprint("@CYN[loading...]", end="\r")
tprint_e(*args, **kwargs)
Print styled text to standard error.
Behaves like tprint() but forces file=sys.stderr.
- Args:
*args: Values joined with spaces and converted to styled text.**kwargs: Additional keyword arguments forwarded toprint;fileis forced tosys.stderr.
tprint_e("@RED[error]@ something went wrong")
Tinting
The markup-based terminal coloring engine class.
Holds the color palette, the compiled token pattern, and precomputed ANSI
escape sequences used by the module-level helpers. It is initialized once at
import time via init_static_class, which reads the environment to decide
whether colors should be enabled and compiles the token regex from the palette
keys.
Class attributes:
| Attribute | Type | Description |
|---|---|---|
colors |
bool |
Whether ANSI color output is enabled. |
pattern |
re.Pattern[str] |
Compiled regex that splits markup tokens. |
_colors |
dict[str, tuple] |
code -> (name, 256_code) palette. |
_color_codes |
dict[str, str] |
code -> 256_code. |
fg |
dict[str, str] |
code -> ANSI foreground escape (\033[38;5;Nm). |
bg |
dict[str, str] |
code -> ANSI background escape (\033[48;5;Nm). |
by_name |
dict[str, str] |
human-readable name -> code. |
from tinting import Tinting
Tinting.by_name["green"] # "GRN"
Tinting.fg["RED"] # "\033[38;5;124m"
Tinting.colors # True/False
TA — Tinting Advanced
Grid-oriented style encoding helpers for efficient terminal rendering.
TA provides an interning-based mapping between style tokens (e.g. @RED[)
and small integer codes, plus utilities to generate, merge, and apply
per-cell style information over character grids. This is useful when rendering
TUIs where many cells share the same style and you want to avoid emitting
redundant markup.
Class attributes:
| Attribute | Type | Description |
|---|---|---|
_decode |
list[str] |
Index -> token lookup (index 0 is the default/no-style). |
_encode_map |
dict[str,int] |
Token -> code lookup (interned). |
TA._encode(token)
Map a style token to a reusable integer code.
Tokens are interned so that equal strings share the same code. The first call for a given token assigns the next sequential code; subsequent calls return the cached code.
- Args:
token(str) — the style token (for example@RED[) to intern. - Returns:
int— the integer code assigned to the token, stable across calls.
TA.Style
Per-character style encoding, merging, and rendering tools.
TA.Style.apply(txt, style, start=0, end=None)
Render a run of characters annotated with style codes.
Consecutive characters sharing the same style code are grouped and wrapped
with the matching markup token. Characters with code 0 (default) are emitted
without any markup.
- Args:
txt(list[str]): The list of single-character strings to render.style(list[int]): The per-character style codes aligned withtxt.start(int): The index at which rendering starts. Defaults to0.end(int | None): The index at which rendering stops, orNoneto render until the end oftxt. Defaults toNone.
- Returns:
str— the rendered string with markup tokens applied.
TA.Style.gen_noesc(txt)
Compute the per-character style codes of a styled line.
Parses a single line of markup text and returns a list of integer style codes aligned with the visible characters. Opening tokens push the current style onto a stack; closing tokens pop it.
- Args:
txt(str) — a single line of styled markup text. - Returns:
list[int]— style codes aligned with the visible characters.
TA.Style.gen_grid_noesc(mtx)
Compute style codes for the styled rows of a grid.
Runs gen_noesc on each row and returns only the rows that contain at least
one non-default (non-zero) style code.
- Args:
mtx(list[str]) — the list of styled rows. - Returns:
dict[int, list[int]]— mapping of row index to its style codes, including only rows that contain at least one non-default style.
TA.Style.gen_grid_from_changes(changes, colors=['SLV'])
Build a sparse style patch from changed coordinates.
Given a list of (x, y) coordinates that changed, produces a sparse mapping
cycling through the provided color codes for successive changes.
- Args:
changes(list[tuple[int, int]]): The(x, y)coordinates that changed.colors(list[str]): The color codes cycled over successive changes. Defaults to['SLV'].
- Returns:
dict[int, list[tuple[int, int]]]— mapping of row index to a list of(x, style_code)pairs describing the style overrides.
TA.Style.merge_grid_style(base, patches)
Overlay sparse style patches onto a base style grid.
Returns a new mapping where each patch overrides specific cells of the base. Patches are applied in order; later patches win on conflicting cells.
- Args:
base(dict[int, list[int]]): The base mapping of row index to style codes.patches(list[dict[int, list[tuple[int, int]]]]): The sparse patches applied in order on top of the base.
- Returns:
dict[int, list[int]]— a new mapping of row index to merged style codes.
TA.Style.grid_apply(mtx, base, patches, start_x=0, end_x=0, start_y=0, end_y=0)
Render a subregion of a character grid with styles applied.
Merges the patches over the base, then renders each row in the requested
window via apply(). The window bounds are clamped to the grid dimensions.
- Args:
mtx(list[list[str]]): The grid of single-character strings.base(dict[int, list[int]]): The base style mapping of the grid.patches(list[dict[int, list[tuple[int, int]]]]): The sparse style patches merged over the base.start_x(int): The first column index to render. Defaults to0.end_x(int): The column index at which rendering stops. Defaults to0.start_y(int): The first row index to render. Defaults to0.end_y(int): The row index at which rendering stops. Defaults to0.
- Returns:
list[str]— the list of rendered rows with markup tokens applied.
Environment variables
Color output is auto-detected at import time by init_static_class:
| Variable | Effect |
|---|---|
NO_COLOR |
If set (any value), color output is disabled. |
FORCE_COLOR |
If set (any value), color output is enabled. |
CI |
If set (any value), color output is enabled. |
When none of these are set, color output is enabled only when stdout is a
TTY (stdout.isatty()). NO_COLOR takes precedence over the others. The
result is stored in Tinting.colors and used by tprint() / tprint_e().
Examples
Status messages
from tinting import tprint
tprint("@GRN[SUCCESS]@ Database connected.")
tprint("@YEL[WARNING]@ Memory above 80%.")
tprint("@RED[ERROR]@ Disk write failed.")
tprint("@CYN[INFO]@ Cron job finished in 0.4s.")
Badges with background colors
tprint("@GRN#BLK[ SUCCESS ]@ @YEL#BLK[ WARNING ]@ @RED#WHT[ ERROR ]@")
Nested styles
tprint("@GRN[outer @*RED[inner bold red]@ outer green]@")
Measuring and slicing styled text
from tinting import tlen, tslice
s = "@GRN[hello]@ @RED[world]@"
print(tlen(s)) # 11
print(tslice(s, 5)) # "@GRN[hello]@" (first 5 visible chars, markup kept)
Escaping user input
from tinting import tesc, tprint
user = "@RED[evil]@"
tprint(tesc(user)) # renders literally: @RED[evil]@
Grid rendering with TA
from tinting import TA
TA._encode_map.clear(); TA._decode = [""] # reset interning
grid = [list("hello"), list("world")]
base = {0: TA.Style.gen_noesc("@GRN[hello]@"),
1: TA.Style.gen_noesc("@GRN[world]@")}
patch = TA.Style.gen_grid_from_changes([(1, 0)], colors=["RED"])
rendered = TA.Style.grid_apply(grid, base, [patch],
start_x=0, end_x=5, start_y=0, end_y=2)
for row in rendered:
print(row)
VS Code extension
Tinting tags can be colorized live inside VS Code with the companion
extension tinting
(source) — hover a tag to
see a swatch and change its color on the spot, toggle modifiers, hide tag
markers, and insert snippets from the command palette.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tinting-0.1.1.tar.gz.
File metadata
- Download URL: tinting-0.1.1.tar.gz
- Upload date:
- Size: 18.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
185a057251d34731467c84a42cf30515a8db5cded45e476ad053dde48e00b599
|
|
| MD5 |
9ba5fe1853991b1c0a6e40197fb049bc
|
|
| BLAKE2b-256 |
b6463c731a5a08c9a596c69e9aae987d17ef7b54e0700709336531a511555406
|
File details
Details for the file tinting-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tinting-0.1.1-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b03416aa0f6181637f92914e1fa8ff9dd24da4e16672cb20eb2a2df7ecce7b45
|
|
| MD5 |
d1987b4bdd2e222903f78155aa9f790d
|
|
| BLAKE2b-256 |
1a5f4185ec51aba5dd27a72e64eb4102bb17ca4c1be85f2f7c94a0c0f0dec379
|