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.
Release files for tinting 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tinting-0.1.1.tar.gz | 18.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tinting-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:32.6 kB
Release files / tinting-0.1.1.tar.gz
| Download URL | tinting-0.1.1.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
185a057251d34731467c84a42cf30515a8db5cded45e476ad053dde48e00b599
|
|
BLAKE2b-256 checksum How to use checksums |
b6463c731a5a08c9a596c69e9aae987d17ef7b54e0700709336531a511555406
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|
Release files / tinting-0.1.1-py3-none-any.whl
| Download URL | tinting-0.1.1-py3-none-any.whl |
|---|---|
| Size | 14.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b03416aa0f6181637f92914e1fa8ff9dd24da4e16672cb20eb2a2df7ecce7b45
|
|
BLAKE2b-256 checksum How to use checksums |
1a5f4185ec51aba5dd27a72e64eb4102bb17ca4c1be85f2f7c94a0c0f0dec379
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|