Styling

Click Extra ships its own Style class as a drop-in replacement for cloup.Style (which itself wraps click.style). The runtime contract (calling the instance to apply styling, equality, hashing, with_()) is identical to cloup’s; everything below is purely additive ergonomics.

The class lands automatically when you do from click_extra import Style: __init__.py re-exports it after from cloup import * so the click-extra version takes precedence.

hello

Hex-string color shorthand

Style.fg and Style.bg accept "#rrggbb" or "#rgb" shorthand strings alongside Click’s named colors and RGB tuples. The shorthand is converted to an RGB tuple on construction:

sample
sample
sample
sample

REPL-friendly __repr__ and __str__

The compact __repr__ hides None and falsy attributes and renders RGB tuples as #rrggbb. The __str__ returns the styled word "sample" so print(style) and debugger inspectors visualize what the style does, not just its fields:

Style(fg=#f1fa8c, bold, italic)
sample

Composition operator a | b

The | operator merges two styles. The right operand wins on conflicts; None fields on either side don’t override the other side’s set fields. The reflected __ror__ makes mixing with cloup.Style (or any compatible subclass) symmetric:

Style(fg='magenta', bold, italic)
Style(fg='cyan', bold, italic)

cascade(base): fill gaps from a base style

Where | lets the right operand always win, cascade keeps the instance’s set values and only fills its None fields from base. Useful for theme-inheritance patterns where a derived style should keep its overrides and inherit the rest:

Style(fg='magenta', bold, italic)

TOML/JSON round-trip with to_dict / from_dict

Style.to_dict() emits a plain mapping containing only the set fields, with RGB tuples flattened to #rrggbb strings so the output round-trips through TOML/JSON/YAML untouched. Style.from_dict() is the symmetric loader and rejects unknown keys with TypeError so typos surface immediately:

{'fg': '#f1fa8c', 'bold': True}
{"fg": "#f1fa8c", "bold": true}
True
TypeError('Unknown Style field(s): colour')

to_css(): CSS declaration list

Renders the style as a semicolon-separated CSS declaration list, suitable for inline style="..." attributes on HTML spans. Used by AnsiHtmlFormatter and the theme palette swatches:

color: #f1fa8c; font-weight: bold; font-style: italic
color: #ff6d67; text-decoration: underline line-through
color: cyan; opacity: 0.6

The mapping is:

Attribute

CSS declaration

fg

color: <color>

bg

background-color: <color>

bold

font-weight: bold

italic

font-style: italic

underline

text-decoration: underline

overline

text-decoration: overline

strikethrough

text-decoration: line-through

dim

opacity: 0.6

reverse

filter: invert(1)

underline, overline and strikethrough collapse into a single text-decoration declaration when more than one is set.

from_ansi(): parse ANSI SGR escapes

Given one or more consecutive ANSI SGR escapes (the \x1b[...m sequences Click emits), rebuild a Style instance. Supports the standard 8/16-color codes (30–37, 40–47, 90–97, 100–107), the 38;5;n / 48;5;n 256-color extension, and the 38;2;r;g;b / 48;2;r;g;b 24-bit extension. Reset codes (the full 0 reset, its parameter-less \x1b[m form included, and selective resets like 22, 39 or 49) are ignored, so parsing the full output of a style call recovers that style. Multiple back-to-back escapes (as Click emits when combining colors with attributes) are merged into a single Style:

Style(fg='red', bold)
Style(fg='red', bold)
Style(fg=#f1fa8c)
Style(fg=226)

from_ansi is the inverse of calling the style: parsing the output of Style(fg="red", bold=True)("text") recovers the same style.

split_ansi() and render_ansi(): tokenize ANSI streams

While from_ansi() parses bare escapes, split_ansi() tokenizes a whole string mixing text and escapes. It is a stateful SGR stream parser: each escape updates the current style (full and selective resets honored), and every maximal run of text sharing the same style is yielded as a (Style, text) tuple. Non-SGR escapes (cursor movements, OSC hyperlink wrappers) carry no style information and are removed from the yielded text:

Style(fg='blue') -> 'Monday'
Style() -> ' was '
Style(fg='yellow', bold) -> 'sunny'

render_ansi(text, emitter) builds on it to rewrite a string: unstyled runs pass through verbatim, and each styled run is replaced by whatever markup the emitter callable produces. Styled runs are split at newlines, so no markup wrapper ever crosses a line boundary:

a [styled] word

open_ansi(): splice a fragment into styled text

Styling a fragment closes it with a reset, and that reset also closes whatever styling surrounded the fragment. open_ansi(text) returns the escapes text still leaves in effect, so the surrounding styling can be reopened right after the splice:

'\x1b[33mRain on Monday, sun on \x1b[34mTuesday\x1b[0m\x1b[33m.\x1b[0m'

A full reset empties the accumulator, so a string that closes its own styling reopens nothing:

Help screens are painted in several passes, and each one runs over what the previous ones already styled. open_ansi() is what lets an option name quoted in a deprecation message take its own color without stripping the message’s color from everything after it.

ANSI markup converters

Four ready-made converters translate ANSI styling to markup languages with native styling support. They power the table styles translation, and are just as useful standalone, to export any styled CLI output:

  • ansi_to_html() produces self-contained, inline-CSS <span> tags (also valid in markups accepting embedded HTML, like MediaWiki).

  • ansi_to_jira() produces {color:…} macros and Jira wiki emphasis markers.

  • ansi_to_latex() produces xcolor-based color macros and core LaTeX text macros.

  • ansi_to_textile() produces %{…} spans carrying the style as inline CSS.

<span style="color: blue; font-weight: bold">Summer</span>
{color:blue}*Summer*{color}
\textcolor{blue}{\textbf{Summer}}
%{color: blue; font-weight: bold}Summer%

Each converter maps ANSI attributes to their closest equivalent in the target markup, and silently drops those the target cannot express (like blink in CSS, backgrounds in Jira markup, or dim in LaTeX). Colors named after the 8 base ANSI colors pass through as color keywords; bright variants, 256-color indices and 24-bit values resolve to hex.

contrast_ratio(other): WCAG accessibility check

Returns the WCAG 2.x contrast ratio between this style’s foreground and another style’s foreground. Result is in [1, 21]: 1 = identical colors (no contrast), 21 = maximum contrast (black on white). WCAG AA requires 4.5+ for normal text and 3.0+ for large text; AAA wants 7.0+ and 4.5+ respectively.

19.26
1.47
4.08

Click Extra uses this internally to gate the WCAG legibility floor and AA Large compliance for branded themes.

Equality and hash

Style overrides cloup’s __eq__ and __hash__ to skip the lazily-populated _style_kwargs cache, so two otherwise-identical styles compare equal whether or not either has been called yet:

True
True
True

Shared dataclass round-trip helpers

The serialization machinery Style.to_dict / from_dict relies on three small module-level helpers that codify “walk dataclass fields, skip the cloup _style_kwargs cache, skip default-valued fields, raise on unknown keys”. They’re public so other dataclass-shaped values in click-extra (notably HelpTheme) can reuse them, and so downstream code with similar patterns can build on the same primitives.

fields_to_dict(instance, *, encode=…, keep=…)

Walks every field via dataclasses.fields, applies an optional keep(field, value) -> bool filter (default keeps every non-default field), and passes the surviving values through an encode(field, value) -> encoded_value callback (default identity):

{'r': 255, 'g': 128, 'name': 'orange'}
{'r': 'ff', 'g': '80', 'name': 'orange'}

dict_to_fields(cls, data, *, decode=…)

The symmetric loader. Validates every key in data against cls’s dataclass fields and raises TypeError listing every unknown key, so callers can build a constructor call without an extra pre-validation pass:

Color(r=255, g=0, b=0, name='orange')
TypeError('Unknown Color field(s): red')

click_extra.styling API

        classDiagram
  Style <|-- Style
    

Drop-in replacement for cloup.Style with extra features.

The module name mirrors cloup.styling, the upstream module that hosts the original Style class. Click Extra’s Style is a subclass that keeps cloup’s runtime contract (calling, equality, hashing, with_()) intact and adds:

  • A compact, single-line __repr__ that hides None and falsy attributes and renders RGB tuples as #rrggbb hex.

  • Hex-string color shorthand: Style(fg="#f1fa8c") works alongside Style(fg=(241, 250, 140)).

  • A __str__ that returns the styled word "sample" so REPL prints and debuggers visualize what the style does, not just its fields.

  • A composition operator a | b that merges two styles, with the right operand winning on conflicts.

  • A Style.cascade() method that fills the style’s None fields from a base style without overriding any value already set.

  • Style.to_dict() / Style.from_dict() for round-tripping styles through TOML/JSON/YAML.

  • Style.to_css() for emitting CSS-equivalent declarations: useful for HTML renderings of help screens.

  • Style.from_ansi() for parsing an ANSI SGR escape sequence back into a Style instance.

  • Style.contrast_ratio() returning the WCAG contrast ratio between two foreground colors. Useful for theme designers checking accessibility.

  • split_ansi() and render_ansi() for tokenizing a string mixing text and ANSI escapes into styled runs, then re-rendering those runs through a markup emitter. Wrapping them to a width lives in wrap_ansi(), with the rest of the terminal-grid measures.

  • open_ansi() reporting the escapes a string leaves open, so a styled fragment can be spliced into styled text without closing it.

  • The ansi_to_html(), ansi_to_jira(), ansi_to_latex() and ansi_to_textile() converters, translating ANSI styling to markup languages with native styling support.

click_extra.styling.fields_to_dict(instance, *, encode=<function <lambda>>, keep=<function <lambda>>)[source]

Serialize a dataclass instance to a dict of set fields.

Walks every field via dataclasses.fields(), skips the internal _style_kwargs cache, applies keep to decide which fields are written (default: every non-default field), and passes the surviving values through encode (default: identity).

Parameters:
  • instance (Any) – the dataclass to serialize.

  • encode (Callable[[Any, Any], Any]) – callable (field, value) -> encoded_value applied to every kept value. Use to convert RGB tuples to #rrggbb strings, nested dataclasses to dicts, etc.

  • keep (Callable[[Any, Any], bool]) – callable (field, value) -> bool deciding whether the field is emitted. Default keeps everything that differs from the field’s declared default.

Return type:

dict[str, Any]

click_extra.styling.dict_to_fields(cls, data, *, decode=<function <lambda>>)[source]

Validate data’s keys against cls’s dataclass fields and decode them.

Returns a kwargs dict ready to splat into cls(**kwargs). Raises TypeError listing every unknown key, so callers can build a constructor call without an extra pre-validation pass.

Parameters:
  • cls (type) – the dataclass type whose fields define the legal keys.

  • data (dict[str, Any]) – mapping from field name to a serialized value.

  • decode (Callable[[Any, Any], Any]) – callable (field, raw) -> decoded_value invoked for every recognized key. Default returns the raw value unchanged.

Return type:

dict[str, Any]

click_extra.styling.supports_truecolor()[source]

Whether the terminal is assumed to render 24-bit (truecolor) ANSI.

Drives Style.__call__’s choice between emitting a 24-bit 38;2;r;g;b sequence and quantizing it to the nearest 38;5;n 256-color index, so a branded theme’s RGB colors degrade gracefully on a terminal that cannot display them.

The policy is optimistic: assume truecolor unless the environment positively says otherwise. Precedence, highest first:

  1. COLORTERM of truecolor / 24bit (see _TRUECOLOR_COLORTERMS) keeps 24-bit.

  2. Any other non-empty COLORTERM quantizes: an explicit lower advertisement.

  3. A TERM ending in -16color quantizes: an unambiguous sub-256 terminal. *-256color is deliberately not treated as a downgrade, since truecolor terminals routinely report it while advertising their 24-bit support through COLORTERM instead. Honoring it would strip truecolor from the very terminals this optimistic default protects.

  4. Otherwise keeps 24-bit.

A dumb / unknown TERM never reaches this decision for CLI output: it has already disabled color upstream through resolve_color_env().

Return type:

bool

class click_extra.styling.Style(fg=None, bg=None, bold=None, dim=None, underline=None, overline=None, italic=None, blink=None, reverse=None, strikethrough=None, text_transform=None)[source]

Bases: Style

cloup.Style with extra ergonomics.

See the module docstring for the full list of additions. The runtime contract (calling the instance to apply styling, equality, hashing, with_()) is otherwise identical to cloup.Style.

Todo

Re-scope __eq__() and __hash__() once cloup ships the fix for janluke/cloup#224. Cloup declares its lazy _style_kwargs cache without compare=False, so a cloup.Style stops comparing equal to its twin, and hash() raises TypeError, from its first call onwards. Both methods stay after that fix, for the cross-class comparison against cloup.Style the generated ones refuse: only their cache rationale expires.

Todo

Drop the # type: ignore[assignment] on the fg and bg re-declarations below once cloup widens its own annotations. It types both Optional[str], while click.style takes int | tuple[int, int, int] | str | None, so covering a palette index and an RGB tuple here reads as an incompatible override. Asked upstream at janluke/cloup#222.

fg: str | tuple[int, int, int] | int | None = None

Foreground color: named ANSI string, #rrggbb hex, RGB tuple, or palette index.

bg: str | tuple[int, int, int] | int | None = None

Background color: named ANSI string, #rrggbb hex, RGB tuple, or palette index.

cascade(base)[source]

Return a copy with None fields filled in from base.

The instance’s own non-None values always win: cascade only fills gaps. Useful for theme inheritance: derived.cascade(parent) keeps derived’s overrides and inherits the rest from parent.

Return type:

Style

to_dict()[source]

Serialize to a plain dict with only the set fields.

RGB tuples are emitted as #rrggbb strings so the result round-trips through TOML/JSON/YAML untouched. Pair with from_dict() to rebuild a Style.

Return type:

dict[str, Any]

classmethod from_dict(data)[source]

Build a Style from the plain dict produced by to_dict().

Validates that every key in data names a known Style field and raises TypeError otherwise. Pair with to_dict() to round-trip through TOML/JSON/YAML.

Return type:

Style

to_css()[source]

Render the style as a semicolon-separated CSS declaration list.

Style(fg="#f1fa8c", bold=True).to_css() returns "color: #f1fa8c; font-weight: bold". Suitable for inline style="..." attributes on HTML spans.

Return type:

str

classmethod from_ansi(escape)[source]

Parse one or more consecutive ANSI SGR escapes into a Style.

Supports the standard 8/16-color codes (30–37, 40–47, 90–97, 100–107), the 38;5;n / 48;5;n 256-color extension, and the 38;2;r;g;b / 48;2;r;g;b 24-bit extension. Reset codes (the full 0 reset, its parameter-less \x1b[m form included, and selective resets like 22, 39 or 49) are ignored, so parsing the full output of a style call (trailing reset included) recovers that style. Multiple back-to-back escapes (as click emits when combining colors with attributes: \x1b[31m\x1b[1m) are merged into a single Style.

To tokenize a string mixing text and escapes, with resets honored, see split_ansi().

Return type:

Style

contrast_ratio(other)[source]

Return the WCAG 2.x contrast ratio between this fg and other’s fg.

Result is in [1, 21]: 1 = identical colors (no contrast), 21 = maximum contrast (black on white). WCAG AA requires 4.5+ for normal text, 3.0+ for large text; AAA wants 7.0+ and 4.5+ respectively.

Return type:

float

click_extra.styling.split_ansi(text)[source]

Split text into (style, text) runs at ANSI SGR escape boundaries.

A stateful SGR stream parser: each escape updates the current style state (with full and selective resets honored, unlike Style.from_ansi()), and every maximal run of text sharing the same state is yielded with its Style. Unstyled text is yielded with an empty Style. Consecutive runs with equal styles are merged and empty runs are dropped, so wrapping each yielded run produces minimal markup.

Non-SGR escapes carry no style information and are removed from the yielded text, per _strip_unsupported_ansi().

Return type:

Iterator[tuple[Style, str]]

click_extra.styling.open_ansi(text, opened='')[source]

Return the ANSI SGR escapes still in effect at the end of text.

Escapes accumulate in the order they were read. A full 0 reset (its parameter-less \x1b[m form included) empties the accumulator, so the result re-opens what text left dangling and nothing else. Escapes come back verbatim rather than folded into a Style, which keeps a code this module does not model.

Use it to splice a styled fragment into styled text: the fragment closes with a reset of its own, which would otherwise strip the surrounding styling from everything after it. See highlight().

Parameters:
  • text (str) – the string to read the escapes from.

  • opened (str) – escapes already in effect before text, as returned by an earlier call. Feeding a string one chunk at a time then costs one pass over it, where re-reading the whole prefix per chunk costs one per chunk. A chunk must not start or end in the middle of an escape: the two halves match nothing, and the escape is lost.

Return type:

str

click_extra.styling.render_ansi(text, emitter)[source]

Rebuild text, replacing each ANSI-styled run by emitter’s markup.

Unstyled runs pass through verbatim, so any markup surrounding the styled runs (table borders, tags produced by another renderer) is preserved byte for byte. Styled runs are handed to emitter one line at a time: runs are split on newlines so no markup wrapper ever crosses a line boundary, which keeps line-oriented markup (LaTeX rows, wiki tables) well-formed even when a style spans multiple lines.

Return type:

str

click_extra.styling.ansi_to_html(text)[source]

Translate ANSI styling in text to inline-styled HTML <span> tags.

\x1b[34mSummer\x1b[0m becomes <span style="color: blue">Summer</span>. The spans are self-contained (no stylesheet needed) and also valid in markups accepting inline HTML, like MediaWiki.

Return type:

str

click_extra.styling.ansi_to_jira(text)[source]

Translate ANSI styling in text to Jira wiki markup.

\x1b[34;1mSummer\x1b[0m becomes {color:blue}*Summer*{color}.

Return type:

str

click_extra.styling.ansi_to_latex(text)[source]

Translate ANSI styling in text to LaTeX macros.

\x1b[34;1mSummer\x1b[0m becomes \textcolor{blue}{\textbf{Summer}}. The colored macros require \usepackage{xcolor} in the document preamble.

Return type:

str

click_extra.styling.ansi_to_textile(text)[source]

Translate ANSI styling in text to Textile spans.

\x1b[34;1mSummer\x1b[0m becomes %{color: blue; font-weight: bold}Summer%.

Return type:

str