Skip to main content

Markup-based terminal coloring engine

Project description

Tinting logo

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, and CI environment 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): When False, markup tokens are stripped without emitting ANSI codes, yielding plain text. Defaults to True.
  • 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): When True, escaped markers use % characters (%COLOR[ / ]%) instead of the pipe-escaped variants, producing plain readable text without pipes. Defaults to False.
  • 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 to 0.
    • start (int): The visible character index at which the slice starts. A negative value is treated as its absolute value. Defaults to 0.
  • 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 to print (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 to print; file is forced to sys.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 with txt.
    • start (int): The index at which rendering starts. Defaults to 0.
    • end (int | None): The index at which rendering stops, or None to render until the end of txt. Defaults to None.
  • 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 to 0.
    • end_x (int): The column index at which rendering stops. Defaults to 0.
    • start_y (int): The first row index to render. Defaults to 0.
    • end_y (int): The row index at which rendering stops. Defaults to 0.
  • 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


Download files

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

Source Distribution

tinting-0.1.1.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

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

tinting-0.1.1-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

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

Hashes for tinting-0.1.1.tar.gz
Algorithm Hash digest
SHA256 185a057251d34731467c84a42cf30515a8db5cded45e476ad053dde48e00b599
MD5 9ba5fe1853991b1c0a6e40197fb049bc
BLAKE2b-256 b6463c731a5a08c9a596c69e9aae987d17ef7b54e0700709336531a511555406

See more details on using hashes here.

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

Hashes for tinting-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b03416aa0f6181637f92914e1fa8ff9dd24da4e16672cb20eb2a2df7ecce7b45
MD5 d1987b4bdd2e222903f78155aa9f790d
BLAKE2b-256 1a5f4185ec51aba5dd27a72e64eb4102bb17ca4c1be85f2f7c94a0c0f0dec379

See more details on using hashes here.

Supported by

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