Metadata-Version: 2.4
Name: tinting
Version: 0.1.1
Summary: Markup-based terminal coloring engine
Author-email: Italo Almeida <italoalmeida@hezz.it>
License: MIT
Project-URL: Homepage, https://github.com/italoalmeida0/tinting
Project-URL: Repository, https://github.com/italoalmeida0/tinting
Project-URL: Issues, https://github.com/italoalmeida0/tinting/issues
Keywords: ansi,terminal,color,colour,markup,tui,cli,256-color
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Terminals
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/italoalmeida0/tinting/main/media/icon.png" alt="Tinting logo" width="256" height="256" />
</p>

<h1 align="center">tinting</h1>

<p align="center">
  <strong>Markup-based terminal coloring engine.</strong>
</p>

`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

```bash
pip install tinting
```

Requires Python 3.9+.

## Quick start

```python
from tinting import tprint

tprint("@GRN[success]@ @YEL#[ warning ]@ @*RED[bold red]@")
```

```python
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.

```python
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.

```python
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.

```python
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.

```python
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`).

```python
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`.

```python
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.                         |

```python
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

```python
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

```python
tprint("@GRN#BLK[ SUCCESS ]@ @YEL#BLK[ WARNING ]@ @RED#WHT[ ERROR ]@")
```

### Nested styles

```python
tprint("@GRN[outer @*RED[inner bold red]@ outer green]@")
```

### Measuring and slicing styled text

```python
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

```python
from tinting import tesc, tprint

user = "@RED[evil]@"
tprint(tesc(user))      # renders literally: @RED[evil]@
```

### Grid rendering with TA

```python
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`](https://marketplace.visualstudio.com/items?itemName=ItaloAlmeida.tinting)
([source](https://github.com/italoalmeida0/vscode-tinting)) — 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](LICENSE).
