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 the style, equality, hashing) 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.
[31m[1mhello[0m
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:
[38;2;241;250;140msample[0m
[38;2;170;187;204msample[0m
[38;2;241;250;140msample[0m
[93msample[0m
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)
[38;2;241;250;140m[1m[3msample[0m
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()producesxcolor-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¶
A Style compares equal to the cloup.Style carrying the same fields, and hashes the same, so the two classes mix in sets and dict keys:
True
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 the instance to apply the style, equality,
hashing) intact and adds:
An equality and a hash shared with
cloup.Style, so aStyleequals thecloup.Stylecarrying the same fields.A compact, single-line
__repr__that hidesNoneand falsy attributes and renders RGB tuples as#rrggbbhex.Hex-string color shorthand:
Style(fg="#f1fa8c")works alongsideStyle(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 | bthat merges two styles, with the right operand winning on conflicts.A
Style.cascade()method that fills the style’sNonefields 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 aStyleinstance.Style.contrast_ratio()returning the WCAG contrast ratio between two foreground colors. Useful for theme designers checking accessibility.split_ansi()andrender_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 inwrap_ansi(), with the rest of the terminal-grid measures.The
ansi_to_html(),ansi_to_jira(),ansi_to_latex()andansi_to_textile()converters, translating ANSI styling to markup languages with native styling support.
The escape grammar, the SGR decoder and the color conversions all of this reads
through live in click_extra.ansi, with
strip_ansi() and open_ansi().
- click_extra.styling.fields_to_dict(instance, *, encode=<function <lambda>>, keep=<function <lambda>>)[source]
Serialize a dataclass instance to a dict of its non-default fields.
Skips every field still holding its declared default, then applies keep to decide which of the remaining fields are written, and passes their values through encode.
- Parameters:
instance (
Any) – the dataclass to serialize.encode (
Callable[[Any,Any],Any]) – callable(field, value) -> encoded_valueapplied to every kept value. Use to convert RGB tuples to#rrggbbstrings, nested dataclasses to dicts, etc. Defaults to the identity.keep (
Callable[[Any,Any],bool]) – callable(field, value) -> booldeciding whether a non-default field is emitted. Defaults to keeping all of them.
- Return type:
- 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).- Parameters:
- Raises:
TypeError – listing every key that names no field of cls, so callers can build a constructor call without validating first.
- Return type:
- 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-bit38;2;r;g;bsequence and quantizing it to the nearest38;5;n256-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:
COLORTERMoftruecolor/24bit(see_TRUECOLOR_COLORTERMS) keeps 24-bit.Any other non-empty
COLORTERMquantizes: an explicit lower advertisement.A
TERMending in-16colorquantizes: an unambiguous sub-256 terminal.*-256coloris deliberately not treated as a downgrade, since truecolor terminals routinely report it while advertising their 24-bit support throughCOLORTERMinstead. Honoring it would strip truecolor from the very terminals this optimistic default protects.Otherwise keeps 24-bit.
A
dumb/unknownTERMnever reaches this decision for CLI output: it has already disabled color upstream throughresolve_color_env().- Return type:
- 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:
Stylecloup.Stylewith extra ergonomics.See the module docstring for the full list of additions. The runtime contract (calling the instance to apply the style, equality, hashing) is otherwise identical to
cloup.Style.- fg: str | tuple[int, int, int] | int | None = None
Foreground color: named ANSI string,
#rrggbbhex, RGB tuple, or palette index.
- bg: str | tuple[int, int, int] | int | None = None
Background color: named ANSI string,
#rrggbbhex, RGB tuple, or palette index.
- cascade(base)[source]
Return a copy with
Nonefields filled in from base.The instance’s own non-
Nonevalues always win:cascadeonly fills gaps. Useful for theme inheritance:derived.cascade(parent)keepsderived’s overrides and inherits the rest fromparent.- Return type:
- to_dict()[source]
Serialize to a plain dict with only the set fields.
RGB tuples are emitted as
#rrggbbstrings so the result round-trips through TOML/JSON/YAML untouched. Pair withfrom_dict()to rebuild aStyle.
- classmethod from_dict(data)[source]
Build a
Stylefrom the plain dict produced byto_dict().Validates that every key in data names a known
Stylefield and raisesTypeErrorotherwise. Pair withto_dict()to round-trip through TOML/JSON/YAML.- Return type:
- 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 inlinestyle="..."attributes on HTML spans.- Return type:
- 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;n256-color extension, and the38;2;r;g;b/48;2;r;g;b24-bit extension. Reset codes (the full0reset, its parameter-less\x1b[mform included, and selective resets like22,39or49) 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 singleStyle.To tokenize a string mixing text and escapes, with resets honored, see
split_ansi().- Return type:
- 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:
- 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 itsStyle. Unstyled text is yielded with an emptyStyle. Consecutive runs with equal styles are merged and empty runs are dropped, so wrapping each yielded run produces minimal markup.Every other escape carries no style information and is removed from the yielded text by
strip_ansi(), so a terminal hyperlink keeps its display text and loses its URI.
- 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:
- click_extra.styling.ansi_to_html(text)[source]
Translate ANSI styling in text to inline-styled HTML
<span>tags.\x1b[34mSummer\x1b[0mbecomes<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:
- click_extra.styling.ansi_to_jira(text)[source]
Translate ANSI styling in text to Jira wiki markup.
\x1b[34;1mSummer\x1b[0mbecomes{color:blue}*Summer*{color}.- Return type:
- click_extra.styling.ansi_to_latex(text)[source]
Translate ANSI styling in text to LaTeX macros.
\x1b[34;1mSummer\x1b[0mbecomes\textcolor{blue}{\textbf{Summer}}. The colored macros require\usepackage{xcolor}in the document preamble.- Return type:
click_extra.ansi API¶
Read and write ANSI escape sequences, and convert terminal colors.
A terminal styles its text with escape sequences mixed into the text. Every module of this package that reads or writes such text uses the grammar and the color arithmetic of this module:
ANSI_ESCAPE_PATTERNmatches one escape sequence of any kind, in ECMA-48 syntax.strip_ansi()removes all of them from a string.ANSI_SGR_REmatches the SGR escapes that set colors and text attributes.sgr_params()andapply_sgr_codes()decode them, andopen_ansi()returns the ones a string leaves in effect.hex_to_rgb(),rgb_to_hex(),palette_to_rgb(),nearest_256()andresolve_rgb()convert a color between the forms a terminal knows: a hex string, a 24-bit RGB triplet, an index of the 256-color palette and a name of the 16 colors ofANSI_16_RGB.
Style, the stream parser
split_ansi() and the markup converters are built on
this module, in click_extra.styling.
- click_extra.ansi.RESET = '\x1b[0m'
Escape closing every style a line left open, written before any padding.
- click_extra.ansi.ANSI_16_RGB: tuple[tuple[int, int, int], ...] = ((0, 0, 0), (239, 41, 41), (138, 226, 52), (252, 233, 79), (52, 101, 164), (197, 9, 197), (52, 226, 226), (245, 245, 245), (103, 103, 103), (255, 109, 103), (95, 249, 103), (254, 251, 103), (104, 113, 255), (255, 118, 255), (95, 253, 255), (254, 255, 255))
Canonical sRGB values for the 16 ANSI colors.
The single source of truth wherever a named ANSI color, or one of the first 16 slots of the 256-color palette, must materialize as a concrete RGB value:
to_css()(bright_*names are not valid CSS keywords),contrast_ratio()(luminance needs RGB),palette_to_rgb()(indices 0-15), and the palettes rendering terminal sessions in the documentation (click_extra.pygments._NAMED_COLORSand_PALETTE_256).Values are the ones the documentation always rendered: Tango-flavored normal colors and iTerm2’s default brights, picked for legibility on both light and dark backgrounds.
Note
The 8 base named colors keep bypassing this table in markup output:
_color_to_css()emits them as plain CSS keywords (and the Jira and LaTeX emitters as macro/xcolor names), delegating the exact shade to the renderer. This table only answers when a concrete value is unavoidable.
- click_extra.ansi.ANSI_NAMES: tuple[str, ...] = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
Names of the 8 base ANSI colors, in palette order.
A bright variant takes the
bright_prefix and the slot 8 places further on.
- click_extra.ansi.ANSI_INDEX: dict[str, int] = {'black': 0, 'blue': 4, 'cyan': 6, 'green': 2, 'magenta': 5, 'red': 1, 'white': 7, 'yellow': 3}
Palette index of each base ANSI color name, the O(1) mirror of
ANSI_NAMESfor the name-to-slot lookups color resolution performs.
- click_extra.ansi.ANSI_SGR_RE: Pattern[str] = re.compile('\\x1b\\[(?P<sgr_params>[0-9;]*)m')
Match a single ANSI SGR escape:
\x1b[...m.The parameter substring may be empty:
\x1b[mis a valid, parameter-less full reset. It is captured assgr_params: the lexer ofclick_extra.pygmentscomposes this pattern into its own tokenizer and reads the parameters by that name.
- click_extra.ansi.SGR_ATTRIBUTES: dict[int, str] = {1: 'bold', 2: 'dim', 3: 'italic', 4: 'underline', 5: 'blink', 7: 'reverse', 9: 'strikethrough', 53: 'overline'}
The SGR code switching each boolean style attribute on.
- click_extra.ansi.is_rgb(color)[source]
Whether
coloris a 24-bit(r, g, b)triplet.The one shape of color this module computes on: a named ANSI color, a palette index or a hex string is resolved to it first.
- click_extra.ansi.hex_to_rgb(value)[source]
Parse a hex color (
#rrggbbor shorthand#rgb) to an RGB tuple.
- click_extra.ansi.rgb_to_hex(rgb)[source]
Format an
(r, g, b)tuple as a#rrggbbhex string.The inverse of
hex_to_rgb(). The single source of truth for the RGB-to-hex rendering shared by__repr__,to_css,to_dict, and the documentation palette.- Return type:
- click_extra.ansi.palette_to_rgb(idx)[source]
Convert a 256-color palette index to its sRGB tuple.
Indices 0-15 are the terminal’s system colors: terminals render them with their theme’s palette, so they resolve through the canonical
ANSI_16_RGBtable. Indices 16-231 (the 6x6x6 color cube) and 232-255 (the grayscale ramp) are fixed by the xterm 256-color layout.
- click_extra.ansi.nearest_256(r, g, b)[source]
Map a 24-bit RGB triplet to the nearest index in the 256-color palette.
The inverse of
palette_to_rgb(). Compares the Euclidean distance in RGB space against both the 6x6x6 color cube (indices 16-231) and the grayscale ramp (indices 232-255), returning whichever is closer.Used by
Style.__call__to downsample branded-theme colors when the terminal lacks truecolor (seesupports_truecolor()), and byclick_extra.pygmentsandclick_extra.clifor the same 24-bit-to-8-bit quantization.See also
Previous implementation of full-color to 8-bit quantization.
- Return type:
- click_extra.ansi.resolve_rgb(color)[source]
Convert a color value to an
(r, g, b)tuple.Accepts RGB tuples, hex strings, named ANSI colors (
"red","bright_blue") and palette indices.- Raises:
ValueError – for any other value.
- Return type:
- click_extra.ansi.CSI_PREFIX_PATTERN = '\\x1b\\[[0-?]*[ -/]*'
A CSI sequence short of its final byte, in ECMA-48 syntax.
ESC [, then the parameter bytes and the intermediate bytes. A stream read in chunks can end on one, the rest of the sequence still to arrive.
- click_extra.ansi.CSI_PATTERN = '\\x1b\\[[0-?]*[ -/]*[@-~]'
One CSI sequence, SGR styles included.
CSI_PREFIX_PATTERN, then a final byte, like themclosing an SGR escape or the~closingESC [ 200 ~.
- click_extra.ansi.ANSI_ESCAPE_PATTERN = '(?:\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b\\[[0-?]*[ -/]*[@-~]|\\x1b[ -/]*[0-Z\\\\^-~])'
One ANSI escape sequence of any kind, SGR styles included, in ECMA-48 syntax.
- click_extra.ansi.strip_ansi(text)[source]
Remove every ANSI escape sequence from
text, leaving its plain text.Styles, cursor movements, screen clearing and OSC commands all go. A terminal hyperlink keeps its display text and loses its URI, which lived in the markers around it. Bytes come back as bytes.
- click_extra.ansi.sgr_params(params)[source]
Decode an SGR parameter substring into a sequence of integer codes.
Per ECMA-48, an empty parameter defaults to
0:\x1b[mis a full reset, and\x1b[;31mcarries a reset followed by a red foreground.
- click_extra.ansi.apply_sgr_codes(codes, state, *, resets=True)[source]
Apply SGR parameter codes to the mutable style state, in place.
The single source of truth for SGR semantics, shared by
split_ansi()(with the defaultresets=True) andfrom_ansi()(withresets=False).Style-setting codes add entries to state, keyed by
Stylefield names. When resets is true, the full0reset clears the state, and selective resets drop their attribute:22(bold and dim),23(italic),24(underline),25(blink),27(reverse),29(strikethrough),39(foreground),49(background) and55(overline). When resets is false all of them are ignored, lettingfrom_ansi()merge escapes without wiping already-parsed codes.Unknown codes are silently skipped, like terminals do.
- Return type:
- click_extra.ansi.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
0reset (its parameter-less\x1b[mform included) empties the accumulator, so the result re-opens what text left dangling and nothing else. Escapes come back verbatim rather than folded into aStyle, 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: